ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__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 +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +102 -74
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +158 -153
- ophyd_async/core/_device.py +143 -115
- ophyd_async/core/_device_filler.py +82 -9
- ophyd_async/core/_flyer.py +16 -7
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +74 -58
- ophyd_async/core/_settings.py +113 -0
- ophyd_async/core/_signal.py +304 -174
- ophyd_async/core/_signal_backend.py +60 -14
- ophyd_async/core/_soft_signal_backend.py +18 -12
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +54 -17
- ophyd_async/core/_utils.py +101 -52
- ophyd_async/core/_yaml_settings.py +66 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +51 -0
- ophyd_async/epics/adandor/_andor_io.py +34 -0
- ophyd_async/epics/adaravis/__init__.py +8 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -41
- ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +36 -14
- ophyd_async/epics/adcore/_core_detector.py +81 -0
- ophyd_async/epics/adcore/_core_io.py +145 -95
- ophyd_async/epics/adcore/_core_logic.py +179 -88
- ophyd_async/epics/adcore/_core_writer.py +223 -0
- ophyd_async/epics/adcore/_hdf_writer.py +51 -92
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +6 -5
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +3 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +32 -27
- ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +7 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +22 -16
- ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +26 -25
- ophyd_async/epics/advimba/_vimba_controller.py +12 -24
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +66 -30
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +50 -18
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +5 -5
- ophyd_async/epics/demo/__init__.py +11 -49
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +83 -38
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +68 -73
- ophyd_async/epics/testing/_utils.py +19 -44
- ophyd_async/epics/testing/test_records.db +16 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -8
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +12 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
- ophyd_async/fastcs/panda/_table.py +13 -7
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +16 -0
- ophyd_async/plan_stubs/_ensure_connected.py +12 -17
- ophyd_async/plan_stubs/_fly.py +3 -5
- ophyd_async/plan_stubs/_nd_attributes.py +9 -5
- ophyd_async/plan_stubs/_panda.py +14 -0
- ophyd_async/plan_stubs/_settings.py +152 -0
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +29 -0
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +21 -33
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +148 -74
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/demo/_counter.py +2 -0
- ophyd_async/tango/demo/_detector.py +2 -0
- ophyd_async/tango/demo/_mover.py +10 -6
- ophyd_async/tango/demo/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +48 -7
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +200 -96
- ophyd_async/testing/_mock_signal_utils.py +59 -73
- ophyd_async/testing/_one_of_everything.py +146 -0
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/demo/_mover.py +0 -95
- ophyd_async/epics/demo/_sensor.py +0 -37
- ophyd_async/epics/demo/sensor.db +0 -19
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
- ophyd_async/sim/demo/_sim_motor.py +0 -107
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
- ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
ophyd_async/core/_signal.py
CHANGED
|
@@ -2,14 +2,17 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
|
+
import inspect
|
|
5
6
|
import time
|
|
6
7
|
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
7
|
-
from typing import Generic, cast
|
|
8
|
+
from typing import Any, Generic, TypeVar, cast
|
|
8
9
|
|
|
9
10
|
from bluesky.protocols import (
|
|
11
|
+
Configurable,
|
|
10
12
|
Locatable,
|
|
11
13
|
Location,
|
|
12
14
|
Movable,
|
|
15
|
+
Reading,
|
|
13
16
|
Status,
|
|
14
17
|
Subscribable,
|
|
15
18
|
)
|
|
@@ -17,18 +20,10 @@ from event_model import DataKey
|
|
|
17
20
|
|
|
18
21
|
from ._device import Device, DeviceConnector
|
|
19
22
|
from ._mock_signal_backend import MockSignalBackend
|
|
20
|
-
from ._protocol import
|
|
21
|
-
|
|
22
|
-
AsyncStageable,
|
|
23
|
-
Reading,
|
|
24
|
-
)
|
|
25
|
-
from ._signal_backend import (
|
|
26
|
-
SignalBackend,
|
|
27
|
-
SignalDatatypeT,
|
|
28
|
-
SignalDatatypeV,
|
|
29
|
-
)
|
|
23
|
+
from ._protocol import AsyncReadable, AsyncStageable
|
|
24
|
+
from ._signal_backend import SignalBackend, SignalDatatypeT, SignalDatatypeV
|
|
30
25
|
from ._soft_signal_backend import SoftSignalBackend
|
|
31
|
-
from ._status import AsyncStatus
|
|
26
|
+
from ._status import AsyncStatus
|
|
32
27
|
from ._utils import (
|
|
33
28
|
CALCULATE_TIMEOUT,
|
|
34
29
|
DEFAULT_TIMEOUT,
|
|
@@ -55,6 +50,8 @@ def _add_timeout(func):
|
|
|
55
50
|
|
|
56
51
|
|
|
57
52
|
class SignalConnector(DeviceConnector):
|
|
53
|
+
"""Used for connecting signals with a given backend."""
|
|
54
|
+
|
|
58
55
|
def __init__(self, backend: SignalBackend):
|
|
59
56
|
self.backend = self._init_backend = backend
|
|
60
57
|
|
|
@@ -69,14 +66,19 @@ class SignalConnector(DeviceConnector):
|
|
|
69
66
|
|
|
70
67
|
class _ChildrenNotAllowed(dict[str, Device]):
|
|
71
68
|
def __setitem__(self, key: str, value: Device) -> None:
|
|
72
|
-
raise
|
|
69
|
+
raise KeyError(
|
|
73
70
|
f"Cannot add Device or Signal child {key}={value} of Signal, "
|
|
74
71
|
"make a subclass of Device instead"
|
|
75
72
|
)
|
|
76
73
|
|
|
77
74
|
|
|
78
75
|
class Signal(Device, Generic[SignalDatatypeT]):
|
|
79
|
-
"""A Device with the concept of a value, with R, RW, W and X flavours
|
|
76
|
+
"""A Device with the concept of a value, with R, RW, W and X flavours.
|
|
77
|
+
|
|
78
|
+
:param backend: The backend for providing Signal values.
|
|
79
|
+
:param timeout: The default timeout for operations on the Signal.
|
|
80
|
+
:param name: The name of the signal.
|
|
81
|
+
"""
|
|
80
82
|
|
|
81
83
|
_connector: SignalConnector
|
|
82
84
|
_child_devices = _ChildrenNotAllowed() # type: ignore
|
|
@@ -92,35 +94,46 @@ class Signal(Device, Generic[SignalDatatypeT]):
|
|
|
92
94
|
|
|
93
95
|
@property
|
|
94
96
|
def source(self) -> str:
|
|
95
|
-
"""
|
|
97
|
+
"""Returns the source of the signal.
|
|
98
|
+
|
|
99
|
+
E.g. "ca://PV_PREFIX:SIGNAL", or "" if not available until connection.
|
|
100
|
+
"""
|
|
96
101
|
return self._connector.backend.source(self.name, read=True)
|
|
97
102
|
|
|
98
103
|
|
|
104
|
+
SignalT = TypeVar("SignalT", bound=Signal)
|
|
105
|
+
|
|
106
|
+
|
|
99
107
|
class _SignalCache(Generic[SignalDatatypeT]):
|
|
100
|
-
def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal):
|
|
101
|
-
self._signal = signal
|
|
108
|
+
def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal) -> None:
|
|
109
|
+
self._signal: Signal[Any] = signal
|
|
102
110
|
self._staged = False
|
|
103
111
|
self._listeners: dict[Callback, bool] = {}
|
|
104
112
|
self._valid = asyncio.Event()
|
|
105
113
|
self._reading: Reading[SignalDatatypeT] | None = None
|
|
106
|
-
self.backend = backend
|
|
114
|
+
self.backend: SignalBackend[SignalDatatypeT] = backend
|
|
107
115
|
signal.log.debug(f"Making subscription on source {signal.source}")
|
|
108
116
|
backend.set_callback(self._callback)
|
|
109
117
|
|
|
110
|
-
def close(self):
|
|
118
|
+
def close(self) -> None:
|
|
111
119
|
self.backend.set_callback(None)
|
|
112
120
|
self._signal.log.debug(f"Closing subscription on source {self._signal.source}")
|
|
113
121
|
|
|
122
|
+
def _ensure_reading(self) -> Reading[SignalDatatypeT]:
|
|
123
|
+
if not self._reading:
|
|
124
|
+
msg = "Monitor not working"
|
|
125
|
+
raise RuntimeError(msg)
|
|
126
|
+
return self._reading
|
|
127
|
+
|
|
114
128
|
async def get_reading(self) -> Reading[SignalDatatypeT]:
|
|
115
129
|
await self._valid.wait()
|
|
116
|
-
|
|
117
|
-
return self._reading
|
|
130
|
+
return self._ensure_reading()
|
|
118
131
|
|
|
119
132
|
async def get_value(self) -> SignalDatatypeT:
|
|
120
|
-
reading = await self.get_reading()
|
|
133
|
+
reading: Reading[SignalDatatypeT] = await self.get_reading()
|
|
121
134
|
return reading["value"]
|
|
122
135
|
|
|
123
|
-
def _callback(self, reading: Reading[SignalDatatypeT]):
|
|
136
|
+
def _callback(self, reading: Reading[SignalDatatypeT]) -> None:
|
|
124
137
|
self._signal.log.debug(
|
|
125
138
|
f"Updated subscription: reading of source {self._signal.source} changed "
|
|
126
139
|
f"from {self._reading} to {reading}"
|
|
@@ -134,12 +147,10 @@ class _SignalCache(Generic[SignalDatatypeT]):
|
|
|
134
147
|
self,
|
|
135
148
|
function: Callback[dict[str, Reading[SignalDatatypeT]] | SignalDatatypeT],
|
|
136
149
|
want_value: bool,
|
|
137
|
-
):
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
else:
|
|
142
|
-
function({self._signal.name: self._reading})
|
|
150
|
+
) -> None:
|
|
151
|
+
function(self._ensure_reading()["value"]) if want_value else function(
|
|
152
|
+
{self._signal.name: self._ensure_reading()}
|
|
153
|
+
)
|
|
143
154
|
|
|
144
155
|
def subscribe(self, function: Callback, want_value: bool) -> None:
|
|
145
156
|
self._listeners[function] = want_value
|
|
@@ -150,13 +161,13 @@ class _SignalCache(Generic[SignalDatatypeT]):
|
|
|
150
161
|
self._listeners.pop(function)
|
|
151
162
|
return self._staged or bool(self._listeners)
|
|
152
163
|
|
|
153
|
-
def set_staged(self, staged: bool):
|
|
164
|
+
def set_staged(self, staged: bool) -> bool:
|
|
154
165
|
self._staged = staged
|
|
155
166
|
return self._staged or bool(self._listeners)
|
|
156
167
|
|
|
157
168
|
|
|
158
169
|
class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribable):
|
|
159
|
-
"""Signal that can be read from and monitored"""
|
|
170
|
+
"""Signal that can be read from and monitored."""
|
|
160
171
|
|
|
161
172
|
_cache: _SignalCache | None = None
|
|
162
173
|
|
|
@@ -167,7 +178,10 @@ class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribab
|
|
|
167
178
|
if cached is None:
|
|
168
179
|
cached = self._cache is not None
|
|
169
180
|
if cached:
|
|
170
|
-
|
|
181
|
+
if not self._cache:
|
|
182
|
+
msg = f"{self.source} not being monitored"
|
|
183
|
+
raise RuntimeError(msg)
|
|
184
|
+
# assert self._cache, f"{self.source} not being monitored"
|
|
171
185
|
return self._cache
|
|
172
186
|
else:
|
|
173
187
|
return self._connector.backend
|
|
@@ -184,46 +198,71 @@ class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribab
|
|
|
184
198
|
|
|
185
199
|
@_add_timeout
|
|
186
200
|
async def read(self, cached: bool | None = None) -> dict[str, Reading]:
|
|
187
|
-
"""Return a single item dict with the reading in it
|
|
201
|
+
"""Return a single item dict with the reading in it.
|
|
202
|
+
|
|
203
|
+
:param cached:
|
|
204
|
+
Whether to use the cached monitored value:
|
|
205
|
+
- If None, use the cache if it exists.
|
|
206
|
+
- If False, do an explicit get.
|
|
207
|
+
- If True, explicitly use the cache and raise an error if it doesn't exist.
|
|
208
|
+
"""
|
|
188
209
|
return {self.name: await self._backend_or_cache(cached).get_reading()}
|
|
189
210
|
|
|
190
211
|
@_add_timeout
|
|
191
212
|
async def describe(self) -> dict[str, DataKey]:
|
|
192
|
-
"""Return a single item dict
|
|
213
|
+
"""Return a single item dict describing the signal value."""
|
|
193
214
|
return {self.name: await self._connector.backend.get_datakey(self.source)}
|
|
194
215
|
|
|
195
216
|
@_add_timeout
|
|
196
217
|
async def get_value(self, cached: bool | None = None) -> SignalDatatypeT:
|
|
197
|
-
"""
|
|
218
|
+
"""Return the current value.
|
|
219
|
+
|
|
220
|
+
:param cached:
|
|
221
|
+
Whether to use the cached monitored value:
|
|
222
|
+
- If None, use the cache if it exists.
|
|
223
|
+
- If False, do an explicit get.
|
|
224
|
+
- If True, explicitly use the cache and raise an error if it doesn't exist.
|
|
225
|
+
"""
|
|
198
226
|
value = await self._backend_or_cache(cached).get_value()
|
|
199
227
|
self.log.debug(f"get_value() on source {self.source} returned {value}")
|
|
200
228
|
return value
|
|
201
229
|
|
|
202
230
|
def subscribe_value(self, function: Callback[SignalDatatypeT]):
|
|
203
|
-
"""Subscribe to updates in value of a device
|
|
231
|
+
"""Subscribe to updates in value of a device.
|
|
232
|
+
|
|
233
|
+
:param function: The callback function to call when the value changes.
|
|
234
|
+
"""
|
|
204
235
|
self._get_cache().subscribe(function, want_value=True)
|
|
205
236
|
|
|
206
|
-
def subscribe(
|
|
207
|
-
|
|
237
|
+
def subscribe(
|
|
238
|
+
self, function: Callback[dict[str, Reading[SignalDatatypeT]]]
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Subscribe to updates in the reading.
|
|
241
|
+
|
|
242
|
+
:param function: The callback function to call when the reading changes.
|
|
243
|
+
"""
|
|
208
244
|
self._get_cache().subscribe(function, want_value=False)
|
|
209
245
|
|
|
210
246
|
def clear_sub(self, function: Callback) -> None:
|
|
211
|
-
"""Remove a subscription
|
|
247
|
+
"""Remove a subscription passed to `subscribe` or `subscribe_value`.
|
|
248
|
+
|
|
249
|
+
:param function: The callback function to remove.
|
|
250
|
+
"""
|
|
212
251
|
self._del_cache(self._get_cache().unsubscribe(function))
|
|
213
252
|
|
|
214
253
|
@AsyncStatus.wrap
|
|
215
254
|
async def stage(self) -> None:
|
|
216
|
-
"""Start caching this signal"""
|
|
255
|
+
"""Start caching this signal."""
|
|
217
256
|
self._get_cache().set_staged(True)
|
|
218
257
|
|
|
219
258
|
@AsyncStatus.wrap
|
|
220
259
|
async def unstage(self) -> None:
|
|
221
|
-
"""Stop caching this signal"""
|
|
260
|
+
"""Stop caching this signal."""
|
|
222
261
|
self._del_cache(self._get_cache().set_staged(False))
|
|
223
262
|
|
|
224
263
|
|
|
225
264
|
class SignalW(Signal[SignalDatatypeT], Movable):
|
|
226
|
-
"""Signal that can be set"""
|
|
265
|
+
"""Signal that can be set."""
|
|
227
266
|
|
|
228
267
|
@AsyncStatus.wrap
|
|
229
268
|
async def set(
|
|
@@ -232,7 +271,12 @@ class SignalW(Signal[SignalDatatypeT], Movable):
|
|
|
232
271
|
wait=True,
|
|
233
272
|
timeout: CalculatableTimeout = CALCULATE_TIMEOUT,
|
|
234
273
|
) -> None:
|
|
235
|
-
"""Set the value and return a status saying when it's done
|
|
274
|
+
"""Set the value and return a status saying when it's done.
|
|
275
|
+
|
|
276
|
+
:param value: The value to set.
|
|
277
|
+
:param wait: If True, wait for the set to complete.
|
|
278
|
+
:param timeout: The timeout for the set.
|
|
279
|
+
"""
|
|
236
280
|
if timeout == CALCULATE_TIMEOUT:
|
|
237
281
|
timeout = self._timeout
|
|
238
282
|
source = self._connector.backend.source(self.name, read=False)
|
|
@@ -242,7 +286,7 @@ class SignalW(Signal[SignalDatatypeT], Movable):
|
|
|
242
286
|
|
|
243
287
|
|
|
244
288
|
class SignalRW(SignalR[SignalDatatypeT], SignalW[SignalDatatypeT], Locatable):
|
|
245
|
-
"""Signal that can be both read and set"""
|
|
289
|
+
"""Signal that can be both read and set."""
|
|
246
290
|
|
|
247
291
|
@_add_timeout
|
|
248
292
|
async def locate(self) -> Location:
|
|
@@ -254,13 +298,17 @@ class SignalRW(SignalR[SignalDatatypeT], SignalW[SignalDatatypeT], Locatable):
|
|
|
254
298
|
|
|
255
299
|
|
|
256
300
|
class SignalX(Signal):
|
|
257
|
-
"""Signal that puts the default value"""
|
|
301
|
+
"""Signal that puts the default value."""
|
|
258
302
|
|
|
259
303
|
@AsyncStatus.wrap
|
|
260
304
|
async def trigger(
|
|
261
305
|
self, wait=True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
|
|
262
306
|
) -> None:
|
|
263
|
-
"""Trigger the action and return a status saying when it's done
|
|
307
|
+
"""Trigger the action and return a status saying when it's done.
|
|
308
|
+
|
|
309
|
+
:param wait: If True, wait for the trigger to complete.
|
|
310
|
+
:param timeout: The timeout for the trigger.
|
|
311
|
+
"""
|
|
264
312
|
if timeout == CALCULATE_TIMEOUT:
|
|
265
313
|
timeout = self._timeout
|
|
266
314
|
source = self._connector.backend.source(self.name, read=False)
|
|
@@ -276,8 +324,15 @@ def soft_signal_rw(
|
|
|
276
324
|
units: str | None = None,
|
|
277
325
|
precision: int | None = None,
|
|
278
326
|
) -> SignalRW[SignalDatatypeT]:
|
|
279
|
-
"""
|
|
327
|
+
"""Create a read-writable Signal with a [](#SoftSignalBackend).
|
|
328
|
+
|
|
280
329
|
May pass metadata, which are propagated into describe.
|
|
330
|
+
|
|
331
|
+
:param datatype: The datatype of the signal.
|
|
332
|
+
:param initial_value: The initial value of the signal.
|
|
333
|
+
:param name: The name of the signal.
|
|
334
|
+
:param units: The units of the signal.
|
|
335
|
+
:param precision: The precision of the signal.
|
|
281
336
|
"""
|
|
282
337
|
backend = SoftSignalBackend(datatype, initial_value, units, precision)
|
|
283
338
|
signal = SignalRW(backend=backend, name=name)
|
|
@@ -291,10 +346,17 @@ def soft_signal_r_and_setter(
|
|
|
291
346
|
units: str | None = None,
|
|
292
347
|
precision: int | None = None,
|
|
293
348
|
) -> tuple[SignalR[SignalDatatypeT], Callable[[SignalDatatypeT], None]]:
|
|
294
|
-
"""
|
|
295
|
-
|
|
349
|
+
"""Create a read-only Signal with a [](#SoftSignalBackend).
|
|
350
|
+
|
|
296
351
|
May pass metadata, which are propagated into describe.
|
|
297
|
-
Use soft_signal_rw if you want a device that is externally modifiable
|
|
352
|
+
Use soft_signal_rw if you want a device that is externally modifiable.
|
|
353
|
+
|
|
354
|
+
:param datatype: The datatype of the signal.
|
|
355
|
+
:param initial_value: The initial value of the signal.
|
|
356
|
+
:param name: The name of the signal.
|
|
357
|
+
:param units: The units of the signal.
|
|
358
|
+
:param precision: The precision of the signal.
|
|
359
|
+
:return: A tuple of the created SignalR and a callable to set its value.
|
|
298
360
|
"""
|
|
299
361
|
backend = SoftSignalBackend(datatype, initial_value, units, precision)
|
|
300
362
|
signal = SignalR(backend=backend, name=name)
|
|
@@ -309,34 +371,34 @@ async def observe_value(
|
|
|
309
371
|
) -> AsyncGenerator[SignalDatatypeT, None]:
|
|
310
372
|
"""Subscribe to the value of a signal so it can be iterated from.
|
|
311
373
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
Call subscribe_value on this at the start, and clear_sub on it at the
|
|
316
|
-
end
|
|
317
|
-
timeout:
|
|
318
|
-
If given, how long to wait for each updated value in seconds. If an update
|
|
319
|
-
is not produced in this time then raise asyncio.TimeoutError
|
|
320
|
-
done_status:
|
|
321
|
-
If this status is complete, stop observing and make the iterator return.
|
|
322
|
-
If it raises an exception then this exception will be raised by the iterator.
|
|
323
|
-
done_timeout:
|
|
324
|
-
If given, the maximum time to watch a signal, in seconds. If the loop is still
|
|
325
|
-
being watched after this length, raise asyncio.TimeoutError. This should be used
|
|
326
|
-
instead of on an 'asyncio.wait_for' timeout
|
|
327
|
-
|
|
328
|
-
Notes
|
|
329
|
-
-----
|
|
330
|
-
Due to a rare condition with busy signals, it is not recommended to use this
|
|
331
|
-
function with asyncio.timeout, including in an 'asyncio.wait_for' loop. Instead,
|
|
332
|
-
this timeout should be given to the done_timeout parameter.
|
|
374
|
+
The first value yielded in the iterator will be the current value of the
|
|
375
|
+
Signal, and subsequent updates from the control system will result in that
|
|
376
|
+
value being yielded, even if it is the same as the previous value.
|
|
333
377
|
|
|
334
|
-
|
|
378
|
+
:param signal:
|
|
379
|
+
Call subscribe_value on this at the start, and clear_sub on it at the end.
|
|
380
|
+
:param timeout:
|
|
381
|
+
If given, how long to wait for each updated value in seconds. If an
|
|
382
|
+
update is not produced in this time then raise asyncio.TimeoutError.
|
|
383
|
+
:param done_status:
|
|
384
|
+
If this status is complete, stop observing and make the iterator return.
|
|
385
|
+
If it raises an exception then this exception will be raised by the
|
|
386
|
+
iterator.
|
|
387
|
+
:param done_timeout:
|
|
388
|
+
If given, the maximum time to watch a signal, in seconds. If the loop is
|
|
389
|
+
still being watched after this length, raise asyncio.TimeoutError. This
|
|
390
|
+
should be used instead of on an 'asyncio.wait_for' timeout.
|
|
335
391
|
|
|
336
|
-
|
|
337
|
-
|
|
392
|
+
Due to a rare condition with busy signals, it is not recommended to use this
|
|
393
|
+
function with asyncio.timeout, including in an `asyncio.wait_for` loop.
|
|
394
|
+
Instead, this timeout should be given to the done_timeout parameter.
|
|
395
|
+
|
|
396
|
+
:example:
|
|
397
|
+
```python
|
|
398
|
+
async for value in observe_value(sig):
|
|
399
|
+
do_something_with(value)
|
|
400
|
+
```
|
|
338
401
|
"""
|
|
339
|
-
|
|
340
402
|
async for _, value in observe_signals_value(
|
|
341
403
|
signal,
|
|
342
404
|
timeout=timeout,
|
|
@@ -359,33 +421,35 @@ async def observe_signals_value(
|
|
|
359
421
|
done_status: Status | None = None,
|
|
360
422
|
done_timeout: float | None = None,
|
|
361
423
|
) -> AsyncGenerator[tuple[SignalR[SignalDatatypeT], SignalDatatypeT], None]:
|
|
362
|
-
"""Subscribe to
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
424
|
+
"""Subscribe to a set of signals so they can be iterated from.
|
|
425
|
+
|
|
426
|
+
The first values yielded in the iterator will be the current values of the
|
|
427
|
+
Signals, and subsequent updates from the control system will result in that
|
|
428
|
+
value being yielded, even if it is the same as the previous value.
|
|
429
|
+
|
|
430
|
+
:param signals:
|
|
431
|
+
Call subscribe_value on all the signals at the start, and clear_sub on
|
|
432
|
+
it at the end.
|
|
433
|
+
:param timeout:
|
|
434
|
+
If given, how long to wait for each updated value in seconds. If an
|
|
435
|
+
update is not produced in this time then raise asyncio.TimeoutError.
|
|
436
|
+
:param done_status:
|
|
373
437
|
If this status is complete, stop observing and make the iterator return.
|
|
374
|
-
If it raises an exception then this exception will be raised by the
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
438
|
+
If it raises an exception then this exception will be raised by the
|
|
439
|
+
iterator.
|
|
440
|
+
:param done_timeout:
|
|
441
|
+
If given, the maximum time to watch a signal, in seconds. If the loop is
|
|
442
|
+
still being watched after this length, raise asyncio.TimeoutError. This
|
|
443
|
+
should be used instead of on an `asyncio.wait_for` timeout.
|
|
444
|
+
|
|
445
|
+
:example:
|
|
446
|
+
```python
|
|
447
|
+
async for signal, value in observe_signals_values(sig1, sig2, ..):
|
|
448
|
+
if signal is sig1:
|
|
449
|
+
do_something_with(value)
|
|
450
|
+
elif signal is sig2:
|
|
451
|
+
do_something_else_with(value)
|
|
452
|
+
```
|
|
389
453
|
"""
|
|
390
454
|
q: asyncio.Queue[tuple[SignalR[SignalDatatypeT], SignalDatatypeT] | Status] = (
|
|
391
455
|
asyncio.Queue()
|
|
@@ -453,29 +517,23 @@ async def wait_for_value(
|
|
|
453
517
|
signal: SignalR[SignalDatatypeT],
|
|
454
518
|
match: SignalDatatypeT | Callable[[SignalDatatypeT], bool],
|
|
455
519
|
timeout: float | None,
|
|
456
|
-
):
|
|
520
|
+
) -> None:
|
|
457
521
|
"""Wait for a signal to have a matching value.
|
|
458
522
|
|
|
459
|
-
|
|
460
|
-
----------
|
|
461
|
-
signal:
|
|
523
|
+
:param signal:
|
|
462
524
|
Call subscribe_value on this at the start, and clear_sub on it at the
|
|
463
|
-
end
|
|
464
|
-
match:
|
|
525
|
+
end.
|
|
526
|
+
:param match:
|
|
465
527
|
If a callable, it should return True if the value matches. If not
|
|
466
528
|
callable then value will be checked for equality with match.
|
|
467
|
-
timeout:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
Or::
|
|
477
|
-
|
|
478
|
-
wait_for_value(device.num_captured, lambda v: v > 45, timeout=1)
|
|
529
|
+
:param timeout: How long to wait for the value to match.
|
|
530
|
+
|
|
531
|
+
:example:
|
|
532
|
+
```python
|
|
533
|
+
await wait_for_value(device.acquiring, 1, timeout=1)
|
|
534
|
+
# or
|
|
535
|
+
await wait_for_value(device.num_captured, lambda v: v > 45, timeout=1)
|
|
536
|
+
```
|
|
479
537
|
"""
|
|
480
538
|
if callable(match):
|
|
481
539
|
checker = _ValueChecker(match, match.__name__) # type: ignore
|
|
@@ -498,28 +556,25 @@ async def set_and_wait_for_other_value(
|
|
|
498
556
|
This function sets a set_signal to a specified set_value and waits for
|
|
499
557
|
a match_signal to have the match_value.
|
|
500
558
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
signal
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
Example usage::
|
|
521
|
-
|
|
522
|
-
set_and_wait_for_value(device.acquire, 1, device.acquire_rbv, 1)
|
|
559
|
+
:param set_signal: The signal to set.
|
|
560
|
+
:param set_value: The value to set it to.
|
|
561
|
+
:param match_signal: The signal to monitor.
|
|
562
|
+
:param match_value:
|
|
563
|
+
The value (or callable that says if the value matches) to wait for.
|
|
564
|
+
:param timeout: How long to wait for the signal to have the value.
|
|
565
|
+
:param set_timeout: How long to wait for the set to complete.
|
|
566
|
+
:param wait_for_set_completion:
|
|
567
|
+
If False then return as soon as the match_signal matches match_value. If
|
|
568
|
+
True then also wait for the set operation to complete before returning.
|
|
569
|
+
|
|
570
|
+
:seealso:
|
|
571
|
+
[](#interact-with-signals)
|
|
572
|
+
|
|
573
|
+
:example:
|
|
574
|
+
To set the setpoint and wait for the readback to match:
|
|
575
|
+
```python
|
|
576
|
+
await set_and_wait_for_value(device.setpoint, 1, device.readback, 1)
|
|
577
|
+
```
|
|
523
578
|
"""
|
|
524
579
|
# Start monitoring before the set to avoid a race condition
|
|
525
580
|
values_gen = observe_value(match_signal)
|
|
@@ -529,25 +584,34 @@ async def set_and_wait_for_other_value(
|
|
|
529
584
|
|
|
530
585
|
status = set_signal.set(set_value, timeout=set_timeout)
|
|
531
586
|
|
|
587
|
+
if callable(match_value):
|
|
588
|
+
matcher: Callable[[SignalDatatypeV], bool] = match_value # type: ignore
|
|
589
|
+
else:
|
|
590
|
+
|
|
591
|
+
def matcher(value):
|
|
592
|
+
return value == match_value
|
|
593
|
+
|
|
594
|
+
matcher.__name__ = f"equals_{match_value}"
|
|
595
|
+
|
|
532
596
|
# If the value was the same as before no need to wait for it to change
|
|
533
|
-
if current_value
|
|
597
|
+
if not matcher(current_value):
|
|
534
598
|
|
|
535
599
|
async def _wait_for_value():
|
|
536
600
|
async for value in values_gen:
|
|
537
|
-
if value
|
|
601
|
+
if matcher(value):
|
|
538
602
|
break
|
|
539
603
|
|
|
540
604
|
try:
|
|
541
605
|
await asyncio.wait_for(_wait_for_value(), timeout)
|
|
542
606
|
if wait_for_set_completion:
|
|
543
607
|
await status
|
|
544
|
-
return status
|
|
545
608
|
except asyncio.TimeoutError as e:
|
|
546
|
-
raise TimeoutError(
|
|
547
|
-
f"{match_signal.name} didn't match
|
|
609
|
+
raise asyncio.TimeoutError(
|
|
610
|
+
f"{match_signal.name} value didn't match value from"
|
|
611
|
+
f" {matcher.__name__}() in {timeout}s"
|
|
548
612
|
) from e
|
|
549
613
|
|
|
550
|
-
return
|
|
614
|
+
return status
|
|
551
615
|
|
|
552
616
|
|
|
553
617
|
async def set_and_wait_for_value(
|
|
@@ -555,37 +619,44 @@ async def set_and_wait_for_value(
|
|
|
555
619
|
value: SignalDatatypeT,
|
|
556
620
|
match_value: SignalDatatypeT | Callable[[SignalDatatypeT], bool] | None = None,
|
|
557
621
|
timeout: float = DEFAULT_TIMEOUT,
|
|
558
|
-
|
|
622
|
+
set_timeout: float | None = None,
|
|
559
623
|
wait_for_set_completion: bool = True,
|
|
560
624
|
) -> AsyncStatus:
|
|
561
|
-
"""Set a signal and monitor
|
|
625
|
+
"""Set a signal and monitor that same signal until it has the specified value.
|
|
562
626
|
|
|
563
|
-
|
|
564
|
-
|
|
627
|
+
This function sets a set_signal to a specified set_value and waits for
|
|
628
|
+
a match_signal to have the match_value.
|
|
629
|
+
|
|
630
|
+
:param signal: The signal to set.
|
|
631
|
+
:param value: The value to set it to.
|
|
632
|
+
:param match_value:
|
|
633
|
+
The value (or callable that says if the value matches) to wait for.
|
|
634
|
+
:param timeout: How long to wait for the signal to have the value.
|
|
635
|
+
:param set_timeout: How long to wait for the set to complete.
|
|
636
|
+
:param wait_for_set_completion:
|
|
637
|
+
If False then return as soon as the match_signal matches match_value. If
|
|
638
|
+
True then also wait for the set operation to complete before returning.
|
|
639
|
+
|
|
640
|
+
:seealso:
|
|
641
|
+
[](#interact-with-signals)
|
|
642
|
+
|
|
643
|
+
:examples:
|
|
644
|
+
To set a parameter and wait for it's value to change:
|
|
645
|
+
```python
|
|
646
|
+
await set_and_wait_for_value(device.parameter, 1)
|
|
647
|
+
```
|
|
648
|
+
For busy record, or other Signals with pattern:
|
|
649
|
+
- Set Signal with `wait=True` and stash the Status
|
|
565
650
|
- Read the same Signal to check the operation has started
|
|
566
651
|
- Return the Status so calling code can wait for operation to complete
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
The expected value of the signal after the operation.
|
|
576
|
-
Used to verify that the set operation was successful.
|
|
577
|
-
timeout:
|
|
578
|
-
How long to wait for the signal to have the value
|
|
579
|
-
status_timeout:
|
|
580
|
-
How long the returned Status will wait for the set to complete
|
|
581
|
-
wait_for_set_completion:
|
|
582
|
-
This will wait for set completion #More info in how-to docs
|
|
583
|
-
|
|
584
|
-
Notes
|
|
585
|
-
-----
|
|
586
|
-
Example usage::
|
|
587
|
-
|
|
588
|
-
set_and_wait_for_value(device.acquire, 1)
|
|
652
|
+
```python
|
|
653
|
+
status = await set_and_wait_for_value(
|
|
654
|
+
device.acquire, 1, wait_for_set_completion=False
|
|
655
|
+
)
|
|
656
|
+
# device is now acquiring
|
|
657
|
+
await status
|
|
658
|
+
# device has finished acquiring
|
|
659
|
+
```
|
|
589
660
|
"""
|
|
590
661
|
if match_value is None:
|
|
591
662
|
match_value = value
|
|
@@ -595,6 +666,65 @@ async def set_and_wait_for_value(
|
|
|
595
666
|
signal,
|
|
596
667
|
match_value,
|
|
597
668
|
timeout,
|
|
598
|
-
|
|
669
|
+
set_timeout,
|
|
599
670
|
wait_for_set_completion,
|
|
600
671
|
)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def walk_rw_signals(device: Device, path_prefix: str = "") -> dict[str, SignalRW[Any]]:
|
|
675
|
+
"""Retrieve all SignalRWs from a device.
|
|
676
|
+
|
|
677
|
+
Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
|
|
678
|
+
part of saving and loading a device.
|
|
679
|
+
|
|
680
|
+
:param device: Device to retrieve read-write signals from.
|
|
681
|
+
:param path_prefix: For internal use, leave blank when calling the method.
|
|
682
|
+
:return:
|
|
683
|
+
A dictionary matching the string attribute path of a SignalRW with the
|
|
684
|
+
signal itself.
|
|
685
|
+
"""
|
|
686
|
+
signals: dict[str, SignalRW[Any]] = {}
|
|
687
|
+
|
|
688
|
+
for attr_name, attr in device.children():
|
|
689
|
+
dot_path = f"{path_prefix}{attr_name}"
|
|
690
|
+
if type(attr) is SignalRW:
|
|
691
|
+
signals[dot_path] = attr
|
|
692
|
+
attr_signals = walk_rw_signals(attr, path_prefix=dot_path + ".")
|
|
693
|
+
signals.update(attr_signals)
|
|
694
|
+
return signals
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
async def walk_config_signals(
|
|
698
|
+
device: Device, path_prefix: str = ""
|
|
699
|
+
) -> dict[str, SignalRW[Any]]:
|
|
700
|
+
"""Retrieve all configuration signals from a device.
|
|
701
|
+
|
|
702
|
+
Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
|
|
703
|
+
part of saving and loading a device.
|
|
704
|
+
|
|
705
|
+
:param device: Device to retrieve configuration signals from.
|
|
706
|
+
:param path_prefix: For internal use, leave blank when calling the method.
|
|
707
|
+
:return:
|
|
708
|
+
A dictionary matching the string attribute path of a SignalRW with the
|
|
709
|
+
signal itself.
|
|
710
|
+
"""
|
|
711
|
+
signals: dict[str, SignalRW[Any]] = {}
|
|
712
|
+
config_names: list[str] = []
|
|
713
|
+
if isinstance(device, Configurable):
|
|
714
|
+
configuration = device.read_configuration()
|
|
715
|
+
if inspect.isawaitable(configuration):
|
|
716
|
+
configuration = await configuration
|
|
717
|
+
config_names = list(configuration.keys())
|
|
718
|
+
for attr_name, attr in device.children():
|
|
719
|
+
dot_path = f"{path_prefix}{attr_name}"
|
|
720
|
+
if isinstance(attr, SignalRW) and attr.name in config_names:
|
|
721
|
+
signals[dot_path] = attr
|
|
722
|
+
signals.update(await walk_config_signals(attr, path_prefix=dot_path + "."))
|
|
723
|
+
|
|
724
|
+
return signals
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
class Ignore:
|
|
728
|
+
"""Annotation to ignore a signal when connecting a device."""
|
|
729
|
+
|
|
730
|
+
pass
|