ophyd-async 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/__init__.py +1 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +91 -19
- ophyd_async/core/_providers.py +68 -0
- ophyd_async/core/async_status.py +90 -42
- ophyd_async/core/detector.py +341 -0
- ophyd_async/core/device.py +226 -0
- ophyd_async/core/device_save_loader.py +286 -0
- ophyd_async/core/flyer.py +85 -0
- ophyd_async/core/mock_signal_backend.py +82 -0
- ophyd_async/core/mock_signal_utils.py +145 -0
- ophyd_async/core/{_device/_signal/signal.py → signal.py} +249 -61
- ophyd_async/core/{_device/_backend/signal_backend.py → signal_backend.py} +12 -5
- ophyd_async/core/{_device/_backend/sim_signal_backend.py → soft_signal_backend.py} +54 -48
- ophyd_async/core/standard_readable.py +261 -0
- ophyd_async/core/utils.py +127 -30
- ophyd_async/epics/_backend/_aioca.py +62 -43
- ophyd_async/epics/_backend/_p4p.py +100 -52
- ophyd_async/epics/_backend/common.py +25 -0
- ophyd_async/epics/areadetector/__init__.py +16 -15
- ophyd_async/epics/areadetector/aravis.py +63 -0
- ophyd_async/epics/areadetector/controllers/__init__.py +5 -0
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +52 -0
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +78 -0
- ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +61 -0
- ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
- ophyd_async/epics/areadetector/drivers/__init__.py +21 -0
- ophyd_async/epics/areadetector/drivers/ad_base.py +107 -0
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +38 -0
- ophyd_async/epics/areadetector/drivers/kinetix_driver.py +27 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +21 -0
- ophyd_async/epics/areadetector/drivers/vimba_driver.py +63 -0
- ophyd_async/epics/areadetector/kinetix.py +46 -0
- ophyd_async/epics/areadetector/pilatus.py +45 -0
- ophyd_async/epics/areadetector/single_trigger_det.py +18 -10
- ophyd_async/epics/areadetector/utils.py +91 -13
- ophyd_async/epics/areadetector/vimba.py +43 -0
- ophyd_async/epics/areadetector/writers/__init__.py +5 -0
- ophyd_async/epics/areadetector/writers/_hdfdataset.py +10 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +54 -0
- ophyd_async/epics/areadetector/writers/hdf_writer.py +142 -0
- ophyd_async/epics/areadetector/writers/nd_file_hdf.py +40 -0
- ophyd_async/epics/areadetector/writers/nd_plugin.py +38 -0
- ophyd_async/epics/demo/__init__.py +78 -51
- ophyd_async/epics/demo/demo_ad_sim_detector.py +35 -0
- ophyd_async/epics/motion/motor.py +67 -52
- ophyd_async/epics/pvi/__init__.py +3 -0
- ophyd_async/epics/pvi/pvi.py +318 -0
- ophyd_async/epics/signal/__init__.py +8 -3
- ophyd_async/epics/signal/signal.py +27 -10
- ophyd_async/log.py +130 -0
- ophyd_async/panda/__init__.py +24 -7
- ophyd_async/panda/_common_blocks.py +49 -0
- ophyd_async/panda/_hdf_panda.py +48 -0
- ophyd_async/panda/_panda_controller.py +37 -0
- ophyd_async/panda/_table.py +158 -0
- ophyd_async/panda/_trigger.py +39 -0
- ophyd_async/panda/_utils.py +15 -0
- ophyd_async/panda/writers/__init__.py +3 -0
- ophyd_async/panda/writers/_hdf_writer.py +220 -0
- ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
- ophyd_async/plan_stubs/__init__.py +13 -0
- ophyd_async/plan_stubs/ensure_connected.py +22 -0
- ophyd_async/plan_stubs/fly.py +149 -0
- ophyd_async/protocols.py +126 -0
- ophyd_async/sim/__init__.py +11 -0
- ophyd_async/sim/demo/__init__.py +3 -0
- ophyd_async/sim/demo/sim_motor.py +103 -0
- ophyd_async/sim/pattern_generator.py +318 -0
- ophyd_async/sim/sim_pattern_detector_control.py +55 -0
- ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
- ophyd_async/sim/sim_pattern_generator.py +37 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/METADATA +35 -67
- ophyd_async-0.3.0.dist-info/RECORD +86 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device/__init__.py +0 -0
- ophyd_async/core/_device/_backend/__init__.py +0 -0
- ophyd_async/core/_device/_signal/__init__.py +0 -0
- ophyd_async/core/_device/device.py +0 -60
- ophyd_async/core/_device/device_collector.py +0 -121
- ophyd_async/core/_device/device_vector.py +0 -14
- ophyd_async/core/_device/standard_readable.py +0 -72
- ophyd_async/epics/areadetector/ad_driver.py +0 -18
- ophyd_async/epics/areadetector/directory_provider.py +0 -18
- ophyd_async/epics/areadetector/hdf_streamer_det.py +0 -167
- ophyd_async/epics/areadetector/nd_file_hdf.py +0 -22
- ophyd_async/epics/areadetector/nd_plugin.py +0 -13
- ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd_async/panda/panda.py +0 -332
- ophyd_async-0.1.0.dist-info/RECORD +0 -45
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -2,24 +2,37 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
AsyncGenerator,
|
|
8
|
+
Callable,
|
|
9
|
+
Dict,
|
|
10
|
+
Generic,
|
|
11
|
+
Mapping,
|
|
12
|
+
Optional,
|
|
13
|
+
Tuple,
|
|
14
|
+
Type,
|
|
15
|
+
Union,
|
|
16
|
+
)
|
|
6
17
|
|
|
7
18
|
from bluesky.protocols import (
|
|
8
|
-
|
|
19
|
+
DataKey,
|
|
20
|
+
Locatable,
|
|
21
|
+
Location,
|
|
9
22
|
Movable,
|
|
10
|
-
Readable,
|
|
11
23
|
Reading,
|
|
12
|
-
|
|
24
|
+
Status,
|
|
13
25
|
Subscribable,
|
|
14
26
|
)
|
|
15
27
|
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
from .._backend.signal_backend import SignalBackend
|
|
19
|
-
from .._backend.sim_signal_backend import SimSignalBackend
|
|
20
|
-
from ..device import Device
|
|
28
|
+
from ophyd_async.core.mock_signal_backend import MockSignalBackend
|
|
29
|
+
from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
from .async_status import AsyncStatus
|
|
32
|
+
from .device import Device
|
|
33
|
+
from .signal_backend import SignalBackend
|
|
34
|
+
from .soft_signal_backend import SoftSignalBackend
|
|
35
|
+
from .utils import DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, Callback, T
|
|
23
36
|
|
|
24
37
|
|
|
25
38
|
def _add_timeout(func):
|
|
@@ -43,34 +56,30 @@ class Signal(Device, Generic[T]):
|
|
|
43
56
|
"""A Device with the concept of a value, with R, RW, W and X flavours"""
|
|
44
57
|
|
|
45
58
|
def __init__(
|
|
46
|
-
self,
|
|
59
|
+
self,
|
|
60
|
+
backend: SignalBackend[T],
|
|
61
|
+
timeout: Optional[float] = DEFAULT_TIMEOUT,
|
|
62
|
+
name: str = "",
|
|
47
63
|
) -> None:
|
|
48
|
-
self._name = ""
|
|
49
64
|
self._timeout = timeout
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if sim:
|
|
61
|
-
self._backend = SimSignalBackend(
|
|
62
|
-
datatype=self._init_backend.datatype, source=self._init_backend.source
|
|
65
|
+
self._initial_backend = self._backend = backend
|
|
66
|
+
super().__init__(name)
|
|
67
|
+
|
|
68
|
+
async def connect(
|
|
69
|
+
self, mock=False, timeout=DEFAULT_TIMEOUT, force_reconnect: bool = False
|
|
70
|
+
):
|
|
71
|
+
if mock and not isinstance(self._backend, MockSignalBackend):
|
|
72
|
+
# Using a soft backend, look to the initial value
|
|
73
|
+
self._backend = MockSignalBackend(
|
|
74
|
+
initial_backend=self._initial_backend,
|
|
63
75
|
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
self._backend = self._init_backend
|
|
67
|
-
_sim_backends.pop(self, None)
|
|
68
|
-
await self._backend.connect()
|
|
76
|
+
self.log.debug(f"Connecting to {self.source}")
|
|
77
|
+
await self._backend.connect(timeout=timeout)
|
|
69
78
|
|
|
70
79
|
@property
|
|
71
80
|
def source(self) -> str:
|
|
72
81
|
"""Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
|
|
73
|
-
return self._backend.source
|
|
82
|
+
return self._backend.source(self.name)
|
|
74
83
|
|
|
75
84
|
__lt__ = __le__ = __eq__ = __ge__ = __gt__ = __ne__ = _fail
|
|
76
85
|
|
|
@@ -89,10 +98,12 @@ class _SignalCache(Generic[T]):
|
|
|
89
98
|
self._value: Optional[T] = None
|
|
90
99
|
|
|
91
100
|
self.backend = backend
|
|
101
|
+
signal.log.debug(f"Making subscription on source {signal.source}")
|
|
92
102
|
backend.set_callback(self._callback)
|
|
93
103
|
|
|
94
104
|
def close(self):
|
|
95
105
|
self.backend.set_callback(None)
|
|
106
|
+
self._signal.log.debug(f"Closing subscription on source {self._signal.source}")
|
|
96
107
|
|
|
97
108
|
async def get_reading(self) -> Reading:
|
|
98
109
|
await self._valid.wait()
|
|
@@ -105,6 +116,10 @@ class _SignalCache(Generic[T]):
|
|
|
105
116
|
return self._value
|
|
106
117
|
|
|
107
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
|
+
)
|
|
108
123
|
self._reading = reading
|
|
109
124
|
self._value = value
|
|
110
125
|
self._valid.set()
|
|
@@ -131,7 +146,7 @@ class _SignalCache(Generic[T]):
|
|
|
131
146
|
return self._staged or bool(self._listeners)
|
|
132
147
|
|
|
133
148
|
|
|
134
|
-
class SignalR(Signal[T],
|
|
149
|
+
class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
|
|
135
150
|
"""Signal that can be read from and monitored"""
|
|
136
151
|
|
|
137
152
|
_cache: Optional[_SignalCache] = None
|
|
@@ -164,14 +179,16 @@ class SignalR(Signal[T], Readable, Stageable, Subscribable):
|
|
|
164
179
|
return {self.name: await self._backend_or_cache(cached).get_reading()}
|
|
165
180
|
|
|
166
181
|
@_add_timeout
|
|
167
|
-
async def describe(self) -> Dict[str,
|
|
182
|
+
async def describe(self) -> Dict[str, DataKey]:
|
|
168
183
|
"""Return a single item dict with the descriptor in it"""
|
|
169
|
-
return {self.name: await self._backend.
|
|
184
|
+
return {self.name: await self._backend.get_datakey(self.source)}
|
|
170
185
|
|
|
171
186
|
@_add_timeout
|
|
172
187
|
async def get_value(self, cached: Optional[bool] = None) -> T:
|
|
173
188
|
"""The current value"""
|
|
174
|
-
|
|
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
|
|
175
192
|
|
|
176
193
|
def subscribe_value(self, function: Callback[T]):
|
|
177
194
|
"""Subscribe to updates in value of a device"""
|
|
@@ -199,44 +216,187 @@ class SignalR(Signal[T], Readable, Stageable, Subscribable):
|
|
|
199
216
|
class SignalW(Signal[T], Movable):
|
|
200
217
|
"""Signal that can be set"""
|
|
201
218
|
|
|
202
|
-
def set(
|
|
219
|
+
def set(
|
|
220
|
+
self, value: T, wait=True, timeout: CalculatableTimeout = CalculateTimeout
|
|
221
|
+
) -> AsyncStatus:
|
|
203
222
|
"""Set the value and return a status saying when it's done"""
|
|
204
|
-
|
|
205
|
-
|
|
223
|
+
if timeout is CalculateTimeout:
|
|
224
|
+
timeout = self._timeout
|
|
225
|
+
|
|
226
|
+
async def do_set():
|
|
227
|
+
self.log.debug(f"Putting value {value} to backend at source {self.source}")
|
|
228
|
+
await self._backend.put(value, wait=wait, timeout=timeout)
|
|
229
|
+
self.log.debug(
|
|
230
|
+
f"Successfully put value {value} to backend at source {self.source}"
|
|
231
|
+
)
|
|
206
232
|
|
|
233
|
+
return AsyncStatus(do_set())
|
|
207
234
|
|
|
208
|
-
|
|
235
|
+
|
|
236
|
+
class SignalRW(SignalR[T], SignalW[T], Locatable):
|
|
209
237
|
"""Signal that can be both read and set"""
|
|
210
238
|
|
|
239
|
+
async def locate(self) -> Location:
|
|
240
|
+
location: Location = {
|
|
241
|
+
"setpoint": await self._backend.get_setpoint(),
|
|
242
|
+
"readback": await self.get_value(),
|
|
243
|
+
}
|
|
244
|
+
return location
|
|
245
|
+
|
|
211
246
|
|
|
212
247
|
class SignalX(Signal):
|
|
213
248
|
"""Signal that puts the default value"""
|
|
214
249
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
250
|
+
def trigger(
|
|
251
|
+
self, wait=True, timeout: CalculatableTimeout = CalculateTimeout
|
|
252
|
+
) -> AsyncStatus:
|
|
253
|
+
"""Trigger the action and return a status saying when it's done"""
|
|
254
|
+
if timeout is CalculateTimeout:
|
|
255
|
+
timeout = self._timeout
|
|
256
|
+
coro = self._backend.put(None, wait=wait, timeout=timeout)
|
|
257
|
+
return AsyncStatus(coro)
|
|
258
|
+
|
|
218
259
|
|
|
260
|
+
def soft_signal_rw(
|
|
261
|
+
datatype: Optional[Type[T]] = None,
|
|
262
|
+
initial_value: Optional[T] = None,
|
|
263
|
+
name: str = "",
|
|
264
|
+
) -> SignalRW[T]:
|
|
265
|
+
"""Creates a read-writable Signal with a SoftSignalBackend"""
|
|
266
|
+
signal = SignalRW(SoftSignalBackend(datatype, initial_value), name=name)
|
|
267
|
+
return signal
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def soft_signal_r_and_setter(
|
|
271
|
+
datatype: Optional[Type[T]] = None,
|
|
272
|
+
initial_value: Optional[T] = None,
|
|
273
|
+
name: str = "",
|
|
274
|
+
) -> Tuple[SignalR[T], Callable[[T], None]]:
|
|
275
|
+
"""Returns a tuple of a read-only Signal and a callable through
|
|
276
|
+
which the signal can be internally modified within the device. Use
|
|
277
|
+
soft_signal_rw if you want a device that is externally modifiable
|
|
278
|
+
"""
|
|
279
|
+
backend = SoftSignalBackend(datatype, initial_value)
|
|
280
|
+
signal = SignalR(backend, name=name)
|
|
219
281
|
|
|
220
|
-
|
|
221
|
-
"""Set the value of a signal that is in sim mode."""
|
|
222
|
-
_sim_backends[signal]._set_value(value)
|
|
282
|
+
return (signal, backend.set_value)
|
|
223
283
|
|
|
224
284
|
|
|
225
|
-
def
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
285
|
+
def _generate_assert_error_msg(
|
|
286
|
+
name: str, expected_result: str, actuall_result: str
|
|
287
|
+
) -> str:
|
|
288
|
+
WARNING = "\033[93m"
|
|
289
|
+
FAIL = "\033[91m"
|
|
290
|
+
ENDC = "\033[0m"
|
|
291
|
+
return (
|
|
292
|
+
f"Expected {WARNING}{name}{ENDC} to produce"
|
|
293
|
+
+ f"\n{FAIL}{actuall_result}{ENDC}"
|
|
294
|
+
+ f"\nbut actually got \n{FAIL}{expected_result}{ENDC}"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
async def assert_value(signal: SignalR[T], value: Any) -> None:
|
|
299
|
+
"""Assert a signal's value and compare it an expected signal.
|
|
300
|
+
|
|
301
|
+
Parameters
|
|
302
|
+
----------
|
|
303
|
+
signal:
|
|
304
|
+
signal with get_value.
|
|
305
|
+
value:
|
|
306
|
+
The expected value from the signal.
|
|
307
|
+
|
|
308
|
+
Notes
|
|
309
|
+
-----
|
|
310
|
+
Example usage::
|
|
311
|
+
await assert_value(signal, value)
|
|
312
|
+
|
|
313
|
+
"""
|
|
314
|
+
actual_value = await signal.get_value()
|
|
315
|
+
assert actual_value == value, _generate_assert_error_msg(
|
|
316
|
+
signal.name, value, actual_value
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
async def assert_reading(
|
|
321
|
+
readable: AsyncReadable, expected_reading: Mapping[str, Reading]
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Assert readings from readable.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
readable:
|
|
328
|
+
Callable with readable.read function that generate readings.
|
|
232
329
|
|
|
330
|
+
reading:
|
|
331
|
+
The expected readings from the readable.
|
|
233
332
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
333
|
+
Notes
|
|
334
|
+
-----
|
|
335
|
+
Example usage::
|
|
336
|
+
await assert_reading(readable, reading)
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
actual_reading = await readable.read()
|
|
340
|
+
assert expected_reading == actual_reading, _generate_assert_error_msg(
|
|
341
|
+
readable.name, expected_reading, actual_reading
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
async def assert_configuration(
|
|
346
|
+
configurable: AsyncConfigurable,
|
|
347
|
+
configuration: Mapping[str, Reading],
|
|
348
|
+
) -> None:
|
|
349
|
+
"""Assert readings from Configurable.
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
configurable:
|
|
354
|
+
Configurable with Configurable.read function that generate readings.
|
|
355
|
+
|
|
356
|
+
configuration:
|
|
357
|
+
The expected readings from configurable.
|
|
358
|
+
|
|
359
|
+
Notes
|
|
360
|
+
-----
|
|
361
|
+
Example usage::
|
|
362
|
+
await assert_configuration(configurable configuration)
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
actual_configurable = await configurable.read_configuration()
|
|
366
|
+
assert configuration == actual_configurable, _generate_assert_error_msg(
|
|
367
|
+
configurable.name, configuration, actual_configurable
|
|
368
|
+
)
|
|
237
369
|
|
|
238
370
|
|
|
239
|
-
|
|
371
|
+
def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
|
|
372
|
+
"""Assert emitted document generated by running a Bluesky plan
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
Doc:
|
|
377
|
+
A dictionary
|
|
378
|
+
|
|
379
|
+
numbers:
|
|
380
|
+
expected emission in kwarg from
|
|
381
|
+
|
|
382
|
+
Notes
|
|
383
|
+
-----
|
|
384
|
+
Example usage::
|
|
385
|
+
assert_emitted(docs, start=1, descriptor=1,
|
|
386
|
+
resource=1, datum=1, event=1, stop=1)
|
|
387
|
+
"""
|
|
388
|
+
assert list(docs) == list(numbers), _generate_assert_error_msg(
|
|
389
|
+
"documents", list(numbers), list(docs)
|
|
390
|
+
)
|
|
391
|
+
actual_numbers = {name: len(d) for name, d in docs.items()}
|
|
392
|
+
assert actual_numbers == numbers, _generate_assert_error_msg(
|
|
393
|
+
"emitted", numbers, actual_numbers
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
async def observe_value(
|
|
398
|
+
signal: SignalR[T], timeout: float | None = None, done_status: Status | None = None
|
|
399
|
+
) -> AsyncGenerator[T, None]:
|
|
240
400
|
"""Subscribe to the value of a signal so it can be iterated from.
|
|
241
401
|
|
|
242
402
|
Parameters
|
|
@@ -244,6 +404,12 @@ async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
|
|
|
244
404
|
signal:
|
|
245
405
|
Call subscribe_value on this at the start, and clear_sub on it at the
|
|
246
406
|
end
|
|
407
|
+
timeout:
|
|
408
|
+
If given, how long to wait for each updated value in seconds. If an update
|
|
409
|
+
is not produced in this time then raise asyncio.TimeoutError
|
|
410
|
+
done_status:
|
|
411
|
+
If this status is complete, stop observing and make the iterator return.
|
|
412
|
+
If it raises an exception then this exception will be raised by the iterator.
|
|
247
413
|
|
|
248
414
|
Notes
|
|
249
415
|
-----
|
|
@@ -252,18 +418,36 @@ async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
|
|
|
252
418
|
async for value in observe_value(sig):
|
|
253
419
|
do_something_with(value)
|
|
254
420
|
"""
|
|
255
|
-
|
|
421
|
+
|
|
422
|
+
q: asyncio.Queue[T | Status] = asyncio.Queue()
|
|
423
|
+
if timeout is None:
|
|
424
|
+
get_value = q.get
|
|
425
|
+
else:
|
|
426
|
+
|
|
427
|
+
async def get_value():
|
|
428
|
+
return await asyncio.wait_for(q.get(), timeout)
|
|
429
|
+
|
|
430
|
+
if done_status is not None:
|
|
431
|
+
done_status.add_callback(q.put_nowait)
|
|
432
|
+
|
|
256
433
|
signal.subscribe_value(q.put_nowait)
|
|
257
434
|
try:
|
|
258
435
|
while True:
|
|
259
|
-
|
|
436
|
+
item = await get_value()
|
|
437
|
+
if done_status and item is done_status:
|
|
438
|
+
if exc := done_status.exception():
|
|
439
|
+
raise exc
|
|
440
|
+
else:
|
|
441
|
+
break
|
|
442
|
+
else:
|
|
443
|
+
yield item
|
|
260
444
|
finally:
|
|
261
445
|
signal.clear_sub(q.put_nowait)
|
|
262
446
|
|
|
263
447
|
|
|
264
448
|
class _ValueChecker(Generic[T]):
|
|
265
449
|
def __init__(self, matcher: Callable[[T], bool], matcher_name: str):
|
|
266
|
-
self._last_value: Optional[T]
|
|
450
|
+
self._last_value: Optional[T] = None
|
|
267
451
|
self._matcher = matcher
|
|
268
452
|
self._matcher_name = matcher_name
|
|
269
453
|
|
|
@@ -273,7 +457,7 @@ class _ValueChecker(Generic[T]):
|
|
|
273
457
|
if self._matcher(value):
|
|
274
458
|
return
|
|
275
459
|
|
|
276
|
-
async def wait_for_value(self, signal: SignalR[T], timeout: float):
|
|
460
|
+
async def wait_for_value(self, signal: SignalR[T], timeout: Optional[float]):
|
|
277
461
|
try:
|
|
278
462
|
await asyncio.wait_for(self._wait_for_value(signal), timeout)
|
|
279
463
|
except asyncio.TimeoutError as e:
|
|
@@ -284,7 +468,7 @@ class _ValueChecker(Generic[T]):
|
|
|
284
468
|
|
|
285
469
|
|
|
286
470
|
async def wait_for_value(
|
|
287
|
-
signal: SignalR[T], match: Union[T, Callable[[T], bool]], timeout: float
|
|
471
|
+
signal: SignalR[T], match: Union[T, Callable[[T], bool]], timeout: Optional[float]
|
|
288
472
|
):
|
|
289
473
|
"""Wait for a signal to have a matching value.
|
|
290
474
|
|
|
@@ -330,6 +514,10 @@ async def set_and_wait_for_value(
|
|
|
330
514
|
- Read the same Signal to check the operation has started
|
|
331
515
|
- Return the Status so calling code can wait for operation to complete
|
|
332
516
|
|
|
517
|
+
This function sets a signal to a specified value, optionally with or without a
|
|
518
|
+
ca/pv put callback, and waits for the readback value of the signal to match the
|
|
519
|
+
value it was set to.
|
|
520
|
+
|
|
333
521
|
Parameters
|
|
334
522
|
----------
|
|
335
523
|
signal:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
2
|
from typing import Generic, Optional, Type
|
|
3
3
|
|
|
4
|
-
from bluesky.protocols import
|
|
4
|
+
from bluesky.protocols import DataKey, Reading
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class SignalBackend(Generic[T]):
|
|
@@ -13,10 +13,13 @@ class SignalBackend(Generic[T]):
|
|
|
13
13
|
datatype: Optional[Type[T]] = None
|
|
14
14
|
|
|
15
15
|
#: Like ca://PV_PREFIX:SIGNAL
|
|
16
|
-
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def source(self, name: str) -> str:
|
|
18
|
+
"""Return source of signal. Signals may pass a name to the backend, which can be
|
|
19
|
+
used or discarded."""
|
|
17
20
|
|
|
18
21
|
@abstractmethod
|
|
19
|
-
async def connect(self):
|
|
22
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT):
|
|
20
23
|
"""Connect to underlying hardware"""
|
|
21
24
|
|
|
22
25
|
@abstractmethod
|
|
@@ -24,7 +27,7 @@ class SignalBackend(Generic[T]):
|
|
|
24
27
|
"""Put a value to the PV, if wait then wait for completion for up to timeout"""
|
|
25
28
|
|
|
26
29
|
@abstractmethod
|
|
27
|
-
async def
|
|
30
|
+
async def get_datakey(self, source: str) -> DataKey:
|
|
28
31
|
"""Metadata like source, dtype, shape, precision, units"""
|
|
29
32
|
|
|
30
33
|
@abstractmethod
|
|
@@ -35,6 +38,10 @@ class SignalBackend(Generic[T]):
|
|
|
35
38
|
async def get_value(self) -> T:
|
|
36
39
|
"""The current value"""
|
|
37
40
|
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def get_setpoint(self) -> T:
|
|
43
|
+
"""The point that a signal was requested to move to."""
|
|
44
|
+
|
|
38
45
|
@abstractmethod
|
|
39
46
|
def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
|
|
40
47
|
"""Observe changes to the current value, timestamp and severity"""
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import inspect
|
|
5
|
-
import re
|
|
6
4
|
import time
|
|
7
5
|
from collections import abc
|
|
8
6
|
from dataclasses import dataclass
|
|
9
7
|
from enum import Enum
|
|
10
|
-
from typing import
|
|
8
|
+
from typing import Dict, Generic, Optional, Type, Union, cast, get_origin
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
import numpy as np
|
|
11
|
+
from bluesky.protocols import DataKey, Dtype, Reading
|
|
13
12
|
|
|
14
|
-
from ...utils import ReadingValueCallback, T, get_dtype
|
|
15
13
|
from .signal_backend import SignalBackend
|
|
14
|
+
from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype
|
|
16
15
|
|
|
17
16
|
primitive_dtypes: Dict[type, Dtype] = {
|
|
18
17
|
str: "string",
|
|
@@ -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
|
|
|
@@ -36,12 +35,17 @@ class SimConverter(Generic[T]):
|
|
|
36
35
|
alarm_severity=-1 if severity > 2 else severity,
|
|
37
36
|
)
|
|
38
37
|
|
|
39
|
-
def
|
|
38
|
+
def get_datakey(self, source: str, value) -> DataKey:
|
|
39
|
+
dtype = type(value)
|
|
40
|
+
if np.issubdtype(dtype, np.integer):
|
|
41
|
+
dtype = int
|
|
42
|
+
elif np.issubdtype(dtype, np.floating):
|
|
43
|
+
dtype = float
|
|
40
44
|
assert (
|
|
41
|
-
|
|
45
|
+
dtype in primitive_dtypes
|
|
42
46
|
), f"invalid converter for value of type {type(value)}"
|
|
43
|
-
|
|
44
|
-
return
|
|
47
|
+
dtype_name = primitive_dtypes[dtype]
|
|
48
|
+
return {"source": source, "dtype": dtype_name, "shape": []}
|
|
45
49
|
|
|
46
50
|
def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
|
|
47
51
|
if datatype is None:
|
|
@@ -50,9 +54,9 @@ class SimConverter(Generic[T]):
|
|
|
50
54
|
return datatype()
|
|
51
55
|
|
|
52
56
|
|
|
53
|
-
class
|
|
54
|
-
def
|
|
55
|
-
return
|
|
57
|
+
class SoftArrayConverter(SoftConverter):
|
|
58
|
+
def get_datakey(self, source: str, value) -> DataKey:
|
|
59
|
+
return {"source": source, "dtype": "array", "shape": [len(value)]}
|
|
56
60
|
|
|
57
61
|
def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
|
|
58
62
|
if datatype is None:
|
|
@@ -65,7 +69,7 @@ class SimArrayConverter(SimConverter):
|
|
|
65
69
|
|
|
66
70
|
|
|
67
71
|
@dataclass
|
|
68
|
-
class
|
|
72
|
+
class SoftEnumConverter(SoftConverter):
|
|
69
73
|
enum_class: Type[Enum]
|
|
70
74
|
|
|
71
75
|
def write_value(self, value: Union[Enum, str]) -> Enum:
|
|
@@ -74,11 +78,9 @@ class SimEnumConverter(SimConverter):
|
|
|
74
78
|
else:
|
|
75
79
|
return self.enum_class(value)
|
|
76
80
|
|
|
77
|
-
def
|
|
81
|
+
def get_datakey(self, source: str, value) -> DataKey:
|
|
78
82
|
choices = [e.value for e in self.enum_class]
|
|
79
|
-
return
|
|
80
|
-
source=source, dtype="string", shape=[], choices=choices
|
|
81
|
-
) # type: ignore
|
|
83
|
+
return {"source": source, "dtype": "string", "shape": [], "choices": choices} # type: ignore
|
|
82
84
|
|
|
83
85
|
def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
|
|
84
86
|
if datatype is None:
|
|
@@ -87,48 +89,50 @@ class SimEnumConverter(SimConverter):
|
|
|
87
89
|
return cast(T, list(datatype.__members__.values())[0]) # type: ignore
|
|
88
90
|
|
|
89
91
|
|
|
90
|
-
class DisconnectedSimConverter(SimConverter):
|
|
91
|
-
def __getattribute__(self, __name: str) -> Any:
|
|
92
|
-
raise NotImplementedError("No PV has been set as connect() has not been called")
|
|
93
|
-
|
|
94
|
-
|
|
95
92
|
def make_converter(datatype):
|
|
96
93
|
is_array = get_dtype(datatype) is not None
|
|
97
94
|
is_sequence = get_origin(datatype) == abc.Sequence
|
|
98
95
|
is_enum = issubclass(datatype, Enum) if inspect.isclass(datatype) else False
|
|
99
96
|
|
|
100
97
|
if is_array or is_sequence:
|
|
101
|
-
return
|
|
98
|
+
return SoftArrayConverter()
|
|
102
99
|
if is_enum:
|
|
103
|
-
return
|
|
100
|
+
return SoftEnumConverter(datatype)
|
|
104
101
|
|
|
105
|
-
return
|
|
102
|
+
return SoftConverter()
|
|
106
103
|
|
|
107
104
|
|
|
108
|
-
class
|
|
109
|
-
"""An
|
|
105
|
+
class SoftSignalBackend(SignalBackend[T]):
|
|
106
|
+
"""An backend to a soft Signal, for test signals see ``MockSignalBackend``."""
|
|
110
107
|
|
|
111
108
|
_value: T
|
|
112
|
-
_initial_value: T
|
|
109
|
+
_initial_value: Optional[T]
|
|
113
110
|
_timestamp: float
|
|
114
111
|
_severity: int
|
|
115
112
|
|
|
116
|
-
def __init__(
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
datatype: Optional[Type[T]],
|
|
116
|
+
initial_value: Optional[T] = None,
|
|
117
|
+
) -> None:
|
|
119
118
|
self.datatype = datatype
|
|
120
|
-
self.
|
|
121
|
-
self.converter:
|
|
122
|
-
self.
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
self._initial_value = initial_value
|
|
120
|
+
self.converter: SoftConverter = make_converter(datatype)
|
|
121
|
+
if self._initial_value is None:
|
|
122
|
+
self._initial_value = self.converter.make_initial_value(self.datatype)
|
|
123
|
+
else:
|
|
124
|
+
self._initial_value = self.converter.write_value(self._initial_value)
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
self.converter = make_converter(self.datatype)
|
|
128
|
-
self._initial_value = self.converter.make_initial_value(self.datatype)
|
|
126
|
+
self.callback: Optional[ReadingValueCallback[T]] = None
|
|
129
127
|
self._severity = 0
|
|
128
|
+
self.set_value(self._initial_value)
|
|
129
|
+
|
|
130
|
+
def source(self, name: str) -> str:
|
|
131
|
+
return f"soft://{name}"
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
134
|
+
"""Connection isn't required for soft signals."""
|
|
135
|
+
pass
|
|
132
136
|
|
|
133
137
|
async def put(self, value: Optional[T], wait=True, timeout=None):
|
|
134
138
|
write_value = (
|
|
@@ -136,13 +140,11 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
136
140
|
if value is not None
|
|
137
141
|
else self._initial_value
|
|
138
142
|
)
|
|
139
|
-
self._set_value(write_value)
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
await asyncio.wait_for(self.put_proceeds.wait(), timeout)
|
|
144
|
+
self.set_value(write_value)
|
|
143
145
|
|
|
144
|
-
def
|
|
145
|
-
"""Method to bypass asynchronous logic
|
|
146
|
+
def set_value(self, value: T):
|
|
147
|
+
"""Method to bypass asynchronous logic."""
|
|
146
148
|
self._value = value
|
|
147
149
|
self._timestamp = time.monotonic()
|
|
148
150
|
reading: Reading = self.converter.reading(
|
|
@@ -152,8 +154,8 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
152
154
|
if self.callback:
|
|
153
155
|
self.callback(reading, self._value)
|
|
154
156
|
|
|
155
|
-
async def
|
|
156
|
-
return self.converter.
|
|
157
|
+
async def get_datakey(self, source: str) -> DataKey:
|
|
158
|
+
return self.converter.get_datakey(source, self._value)
|
|
157
159
|
|
|
158
160
|
async def get_reading(self) -> Reading:
|
|
159
161
|
return self.converter.reading(self._value, self._timestamp, self._severity)
|
|
@@ -161,6 +163,10 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
161
163
|
async def get_value(self) -> T:
|
|
162
164
|
return self.converter.value(self._value)
|
|
163
165
|
|
|
166
|
+
async def get_setpoint(self) -> T:
|
|
167
|
+
"""For a soft signal, the setpoint and readback values are the same."""
|
|
168
|
+
return await self.get_value()
|
|
169
|
+
|
|
164
170
|
def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
|
|
165
171
|
if callback:
|
|
166
172
|
assert not self.callback, "Cannot set a callback when one is already set"
|