ophyd-async 0.14.2__py3-none-any.whl → 0.15__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 +17 -5
- ophyd_async/core/{_table.py → _datatypes.py} +18 -9
- ophyd_async/core/_derived_signal.py +2 -2
- ophyd_async/core/_derived_signal_backend.py +1 -5
- ophyd_async/core/_device_filler.py +4 -6
- ophyd_async/core/_mock_signal_backend.py +25 -7
- ophyd_async/core/_mock_signal_utils.py +7 -11
- ophyd_async/core/_signal.py +11 -11
- ophyd_async/core/_signal_backend.py +7 -19
- ophyd_async/core/_soft_signal_backend.py +6 -6
- ophyd_async/core/_status.py +81 -4
- ophyd_async/core/_typing.py +0 -0
- ophyd_async/core/_utils.py +57 -7
- ophyd_async/epics/adcore/_core_io.py +12 -5
- ophyd_async/epics/adcore/_core_logic.py +1 -1
- ophyd_async/epics/core/__init__.py +2 -1
- ophyd_async/epics/core/_aioca.py +13 -3
- ophyd_async/epics/core/_epics_connector.py +4 -1
- ophyd_async/epics/core/_p4p.py +13 -3
- ophyd_async/epics/core/_signal.py +18 -6
- ophyd_async/epics/core/_util.py +23 -3
- ophyd_async/epics/demo/_motor.py +2 -2
- ophyd_async/epics/motor.py +15 -17
- ophyd_async/epics/odin/_odin_io.py +1 -1
- ophyd_async/epics/pmac/_pmac_io.py +23 -4
- ophyd_async/epics/pmac/_pmac_trajectory.py +47 -10
- ophyd_async/fastcs/eiger/_eiger_io.py +20 -1
- ophyd_async/fastcs/jungfrau/_signals.py +4 -1
- ophyd_async/fastcs/panda/_block.py +28 -6
- ophyd_async/fastcs/panda/_writer.py +1 -3
- ophyd_async/tango/core/_tango_transport.py +7 -17
- ophyd_async/tango/demo/_counter.py +2 -2
- {ophyd_async-0.14.2.dist-info → ophyd_async-0.15.dist-info}/METADATA +1 -1
- {ophyd_async-0.14.2.dist-info → ophyd_async-0.15.dist-info}/RECORD +38 -37
- {ophyd_async-0.14.2.dist-info → ophyd_async-0.15.dist-info}/WHEEL +1 -1
- {ophyd_async-0.14.2.dist-info → ophyd_async-0.15.dist-info}/licenses/LICENSE +0 -0
- {ophyd_async-0.14.2.dist-info → ophyd_async-0.15.dist-info}/top_level.txt +0 -0
ophyd_async/core/_utils.py
CHANGED
|
@@ -5,6 +5,8 @@ import logging
|
|
|
5
5
|
from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from enum import Enum, EnumMeta, StrEnum
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from inspect import isawaitable
|
|
8
10
|
from typing import (
|
|
9
11
|
Any,
|
|
10
12
|
Generic,
|
|
@@ -13,6 +15,7 @@ from typing import (
|
|
|
13
15
|
TypeVar,
|
|
14
16
|
get_args,
|
|
15
17
|
get_origin,
|
|
18
|
+
get_type_hints,
|
|
16
19
|
)
|
|
17
20
|
|
|
18
21
|
import numpy as np
|
|
@@ -204,6 +207,20 @@ async def wait_for_connection(**coros: Awaitable[None]):
|
|
|
204
207
|
raise NotConnectedError.with_other_exceptions_logged(exceptions)
|
|
205
208
|
|
|
206
209
|
|
|
210
|
+
# Cache get_type_hints calls to avoid expensive introspection across the codebase
|
|
211
|
+
@lru_cache(maxsize=512)
|
|
212
|
+
def cached_get_type_hints(cls: type, include_extras: bool = False) -> dict[str, Any]:
|
|
213
|
+
"""Get type hints with caching to avoid expensive introspection."""
|
|
214
|
+
return get_type_hints(cls, include_extras=include_extras)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Cache get_origin calls to avoid expensive type introspection
|
|
218
|
+
@lru_cache(maxsize=512)
|
|
219
|
+
def cached_get_origin(tp: Any) -> Any:
|
|
220
|
+
"""Get the origin of a type with caching."""
|
|
221
|
+
return get_origin(tp)
|
|
222
|
+
|
|
223
|
+
|
|
207
224
|
def get_dtype(datatype: type) -> np.dtype:
|
|
208
225
|
"""Get the runtime dtype from a numpy ndarray type annotation.
|
|
209
226
|
|
|
@@ -215,7 +232,7 @@ def get_dtype(datatype: type) -> np.dtype:
|
|
|
215
232
|
|
|
216
233
|
```
|
|
217
234
|
"""
|
|
218
|
-
if not
|
|
235
|
+
if not cached_get_origin(datatype) == np.ndarray:
|
|
219
236
|
raise TypeError(f"Expected Array1D[dtype], got {datatype}")
|
|
220
237
|
# datatype = numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]
|
|
221
238
|
# so extract numpy.float64 from it
|
|
@@ -240,7 +257,7 @@ def get_enum_cls(datatype: type | None) -> type[EnumTypes] | None:
|
|
|
240
257
|
|
|
241
258
|
```
|
|
242
259
|
"""
|
|
243
|
-
if
|
|
260
|
+
if cached_get_origin(datatype) is Sequence:
|
|
244
261
|
datatype = get_args(datatype)[0]
|
|
245
262
|
datatype = get_origin_class(datatype)
|
|
246
263
|
if datatype and issubclass(datatype, Enum):
|
|
@@ -292,10 +309,36 @@ async def merge_gathered_dicts(
|
|
|
292
309
|
return ret
|
|
293
310
|
|
|
294
311
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
312
|
+
def _partition_awaitable(
|
|
313
|
+
maybe_awaitables: Iterable[T | Awaitable[T]],
|
|
314
|
+
) -> tuple[dict[int, Awaitable[T]], dict[int, T]]:
|
|
315
|
+
awaitable: dict[int, Awaitable[T]] = {}
|
|
316
|
+
not_awaitable: dict[int, T] = {}
|
|
317
|
+
for i, x in enumerate(maybe_awaitables):
|
|
318
|
+
if isawaitable(x):
|
|
319
|
+
awaitable[i] = x
|
|
320
|
+
else:
|
|
321
|
+
not_awaitable[i] = x
|
|
322
|
+
return awaitable, not_awaitable
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
async def gather_dict(coros: dict[T | Awaitable[T], V | Awaitable[V]]) -> dict[T, V]:
|
|
326
|
+
"""Await any coros in the keys or values of a dictionary."""
|
|
327
|
+
k_awaitable, k_not_awaitable = _partition_awaitable(coros.keys())
|
|
328
|
+
v_awaitable, v_not_awaitable = _partition_awaitable(coros.values())
|
|
329
|
+
|
|
330
|
+
# Await all awaitables in parallel
|
|
331
|
+
k_results, v_results = await asyncio.gather(
|
|
332
|
+
asyncio.gather(*k_awaitable.values()),
|
|
333
|
+
asyncio.gather(*v_awaitable.values()),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Combine awaited and non-awaited values by index
|
|
337
|
+
k_map = k_not_awaitable | dict(zip(k_awaitable, k_results, strict=True))
|
|
338
|
+
v_map = v_not_awaitable | dict(zip(v_awaitable, v_results, strict=True))
|
|
339
|
+
|
|
340
|
+
# Reconstruct dict in original index order
|
|
341
|
+
return {k_map[i]: v_map[i] for i in range(len(coros))}
|
|
299
342
|
|
|
300
343
|
|
|
301
344
|
def in_micros(t: float) -> int:
|
|
@@ -310,8 +353,10 @@ def in_micros(t: float) -> int:
|
|
|
310
353
|
return int(np.ceil(t * 1e6))
|
|
311
354
|
|
|
312
355
|
|
|
356
|
+
@lru_cache(maxsize=512)
|
|
313
357
|
def get_origin_class(annotatation: Any) -> type | None:
|
|
314
|
-
origin
|
|
358
|
+
"""Get the origin class of a type annotation with caching."""
|
|
359
|
+
origin = cached_get_origin(annotatation) or annotatation
|
|
315
360
|
if isinstance(origin, type):
|
|
316
361
|
return origin
|
|
317
362
|
return None
|
|
@@ -363,3 +408,8 @@ def error_if_none(value: T | None, msg: str) -> T:
|
|
|
363
408
|
if value is None:
|
|
364
409
|
raise RuntimeError(msg)
|
|
365
410
|
return value
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def non_zero(value):
|
|
414
|
+
"""Return True if the value cast to an int is not zero."""
|
|
415
|
+
return int(value) != 0
|
|
@@ -8,8 +8,9 @@ from ophyd_async.core import (
|
|
|
8
8
|
SignalR,
|
|
9
9
|
SignalRW,
|
|
10
10
|
StrictEnum,
|
|
11
|
+
non_zero,
|
|
11
12
|
)
|
|
12
|
-
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
13
|
+
from ophyd_async.epics.core import EpicsDevice, EpicsOptions, PvSuffix
|
|
13
14
|
|
|
14
15
|
from ._utils import ADBaseDataType, ADFileWriteMode, ADImageMode, convert_ad_dtype_to_np
|
|
15
16
|
|
|
@@ -23,7 +24,7 @@ class NDArrayBaseIO(EpicsDevice):
|
|
|
23
24
|
|
|
24
25
|
unique_id: A[SignalR[int], PvSuffix("UniqueId_RBV")]
|
|
25
26
|
nd_attributes_file: A[SignalRW[str], PvSuffix("NDAttributesFile")]
|
|
26
|
-
acquire: A[SignalRW[bool], PvSuffix.rbv("Acquire")]
|
|
27
|
+
acquire: A[SignalRW[bool], PvSuffix.rbv("Acquire"), EpicsOptions(wait=non_zero)]
|
|
27
28
|
array_size_x: A[SignalR[int], PvSuffix("ArraySizeX_RBV")]
|
|
28
29
|
array_size_y: A[SignalR[int], PvSuffix("ArraySizeY_RBV")]
|
|
29
30
|
data_type: A[SignalR[ADBaseDataType], PvSuffix("DataType_RBV")]
|
|
@@ -200,7 +201,7 @@ class NDFileIO(NDArrayBaseIO):
|
|
|
200
201
|
file_write_mode: A[SignalRW[ADFileWriteMode], PvSuffix.rbv("FileWriteMode")]
|
|
201
202
|
num_capture: A[SignalRW[int], PvSuffix.rbv("NumCapture")]
|
|
202
203
|
num_captured: A[SignalR[int], PvSuffix("NumCaptured_RBV")]
|
|
203
|
-
capture: A[SignalRW[bool], PvSuffix.rbv("Capture")]
|
|
204
|
+
capture: A[SignalRW[bool], PvSuffix.rbv("Capture"), EpicsOptions(wait=non_zero)]
|
|
204
205
|
array_size0: A[SignalR[int], PvSuffix("ArraySize0")]
|
|
205
206
|
array_size1: A[SignalR[int], PvSuffix("ArraySize1")]
|
|
206
207
|
create_directory: A[SignalRW[int], PvSuffix("CreateDirectory")]
|
|
@@ -240,11 +241,17 @@ class NDCBFlushOnSoftTrgMode(StrictEnum):
|
|
|
240
241
|
|
|
241
242
|
|
|
242
243
|
class NDPluginCBIO(NDPluginBaseIO):
|
|
244
|
+
"""Plugin that outputs pre/post-trigger NDArrays based on defined conditions.
|
|
245
|
+
|
|
246
|
+
This mirrors the interface provided by ADCore//Db/NDCircularBuff.template
|
|
247
|
+
See HTML docs at https://areadetector.github.io/areaDetector/ADCore/NDPluginCircularBuff.html
|
|
248
|
+
"""
|
|
249
|
+
|
|
243
250
|
pre_count: A[SignalRW[int], PvSuffix.rbv("PreCount")]
|
|
244
251
|
post_count: A[SignalRW[int], PvSuffix.rbv("PostCount")]
|
|
245
252
|
preset_trigger_count: A[SignalRW[int], PvSuffix.rbv("PresetTriggerCount")]
|
|
246
|
-
trigger: A[SignalRW[bool], PvSuffix.rbv("Trigger")]
|
|
247
|
-
capture: A[SignalRW[bool], PvSuffix.rbv("Capture")]
|
|
253
|
+
trigger: A[SignalRW[bool], PvSuffix.rbv("Trigger"), EpicsOptions(wait=non_zero)]
|
|
254
|
+
capture: A[SignalRW[bool], PvSuffix.rbv("Capture"), EpicsOptions(wait=non_zero)]
|
|
248
255
|
flush_on_soft_trg: A[
|
|
249
256
|
SignalRW[NDCBFlushOnSoftTrgMode], PvSuffix.rbv("FlushOnSoftTrg")
|
|
250
257
|
]
|
|
@@ -218,7 +218,7 @@ class ADBaseContAcqController(ADBaseController[ADBaseIO]):
|
|
|
218
218
|
)
|
|
219
219
|
|
|
220
220
|
# Send the trigger to begin acquisition
|
|
221
|
-
await self.cb_plugin.trigger.set(True
|
|
221
|
+
await self.cb_plugin.trigger.set(True)
|
|
222
222
|
|
|
223
223
|
async def disarm(self) -> None:
|
|
224
224
|
await stop_busy_record(self.cb_plugin.capture, False)
|
|
@@ -10,7 +10,7 @@ from ._signal import (
|
|
|
10
10
|
epics_signal_w,
|
|
11
11
|
epics_signal_x,
|
|
12
12
|
)
|
|
13
|
-
from ._util import stop_busy_record
|
|
13
|
+
from ._util import EpicsOptions, stop_busy_record
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
16
16
|
"PviDeviceConnector",
|
|
@@ -25,4 +25,5 @@ __all__ = [
|
|
|
25
25
|
"epics_signal_w",
|
|
26
26
|
"epics_signal_x",
|
|
27
27
|
"stop_busy_record",
|
|
28
|
+
"EpicsOptions",
|
|
28
29
|
]
|
ophyd_async/epics/core/_aioca.py
CHANGED
|
@@ -36,7 +36,12 @@ from ophyd_async.core import (
|
|
|
36
36
|
wait_for_connection,
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
from ._util import
|
|
39
|
+
from ._util import (
|
|
40
|
+
EpicsOptions,
|
|
41
|
+
EpicsSignalBackend,
|
|
42
|
+
format_datatype,
|
|
43
|
+
get_supported_values,
|
|
44
|
+
)
|
|
40
45
|
|
|
41
46
|
logger = logging.getLogger("ophyd_async")
|
|
42
47
|
|
|
@@ -255,12 +260,13 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
255
260
|
datatype: type[SignalDatatypeT] | None,
|
|
256
261
|
read_pv: str = "",
|
|
257
262
|
write_pv: str = "",
|
|
263
|
+
options: EpicsOptions | None = None,
|
|
258
264
|
):
|
|
259
265
|
self.converter: CaConverter = DisconnectedCaConverter(float, dbr.DBR_DOUBLE)
|
|
260
266
|
self.initial_values: dict[str, AugmentedValue] = {}
|
|
261
267
|
self.subscription: Subscription | None = None
|
|
262
268
|
self._all_updates = _all_updates()
|
|
263
|
-
super().__init__(datatype, read_pv, write_pv)
|
|
269
|
+
super().__init__(datatype, read_pv, write_pv, options)
|
|
264
270
|
|
|
265
271
|
def source(self, name: str, read: bool):
|
|
266
272
|
return f"ca://{self.read_pv if read else self.write_pv}"
|
|
@@ -299,11 +305,15 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
299
305
|
"alarm_severity": -1 if value.severity > 2 else value.severity,
|
|
300
306
|
}
|
|
301
307
|
|
|
302
|
-
async def put(self, value: SignalDatatypeT | None
|
|
308
|
+
async def put(self, value: SignalDatatypeT | None):
|
|
303
309
|
if value is None:
|
|
304
310
|
write_value = self.initial_values[self.write_pv]
|
|
305
311
|
else:
|
|
306
312
|
write_value = self.converter.write_value(value)
|
|
313
|
+
if callable(self.options.wait):
|
|
314
|
+
wait = self.options.wait(value)
|
|
315
|
+
else:
|
|
316
|
+
wait = self.options.wait
|
|
307
317
|
try:
|
|
308
318
|
await caput(
|
|
309
319
|
self.write_pv,
|
|
@@ -5,7 +5,8 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from ophyd_async.core import Device, DeviceConnector, DeviceFiller
|
|
7
7
|
|
|
8
|
-
from ._signal import
|
|
8
|
+
from ._signal import get_signal_backend_type, split_protocol_from_pv
|
|
9
|
+
from ._util import EpicsOptions, EpicsSignalBackend
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@dataclass
|
|
@@ -44,6 +45,8 @@ def fill_backend_with_prefix(
|
|
|
44
45
|
backend.write_pv = prefix + (
|
|
45
46
|
annotation.write_suffix or annotation.read_suffix
|
|
46
47
|
)
|
|
48
|
+
elif isinstance(annotation, EpicsOptions):
|
|
49
|
+
backend.options = annotation
|
|
47
50
|
else:
|
|
48
51
|
unhandled.append(annotation)
|
|
49
52
|
annotations.extend(unhandled)
|
ophyd_async/epics/core/_p4p.py
CHANGED
|
@@ -29,7 +29,12 @@ from ophyd_async.core import (
|
|
|
29
29
|
wait_for_connection,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
from ._util import
|
|
32
|
+
from ._util import (
|
|
33
|
+
EpicsOptions,
|
|
34
|
+
EpicsSignalBackend,
|
|
35
|
+
format_datatype,
|
|
36
|
+
get_supported_values,
|
|
37
|
+
)
|
|
33
38
|
|
|
34
39
|
logger = logging.getLogger("ophyd_async")
|
|
35
40
|
|
|
@@ -347,11 +352,12 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
347
352
|
datatype: type[SignalDatatypeT] | None,
|
|
348
353
|
read_pv: str = "",
|
|
349
354
|
write_pv: str = "",
|
|
355
|
+
options: EpicsOptions | None = None,
|
|
350
356
|
):
|
|
351
357
|
self.converter: PvaConverter = DisconnectedPvaConverter(float)
|
|
352
358
|
self.initial_values: dict[str, Any] = {}
|
|
353
359
|
self.subscription: Subscription | None = None
|
|
354
|
-
super().__init__(datatype, read_pv, write_pv)
|
|
360
|
+
super().__init__(datatype, read_pv, write_pv, options)
|
|
355
361
|
|
|
356
362
|
def source(self, name: str, read: bool):
|
|
357
363
|
return f"pva://{self.read_pv if read else self.write_pv}"
|
|
@@ -380,11 +386,15 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
380
386
|
"alarm_severity": -1 if sv > 2 else sv,
|
|
381
387
|
}
|
|
382
388
|
|
|
383
|
-
async def put(self, value: SignalDatatypeT | None
|
|
389
|
+
async def put(self, value: SignalDatatypeT | None):
|
|
384
390
|
if value is None:
|
|
385
391
|
write_value = self.initial_values[self.write_pv]["value"]
|
|
386
392
|
else:
|
|
387
393
|
write_value = self.converter.write_value(value)
|
|
394
|
+
if callable(self.options.wait):
|
|
395
|
+
wait = self.options.wait(value)
|
|
396
|
+
else:
|
|
397
|
+
wait = self.options.wait
|
|
388
398
|
await context().put(self.write_pv, {"value": write_value}, wait=wait)
|
|
389
399
|
|
|
390
400
|
async def get_datakey(self, source: str) -> DataKey:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from enum import Enum
|
|
6
7
|
|
|
7
8
|
from ophyd_async.core import (
|
|
@@ -15,7 +16,7 @@ from ophyd_async.core import (
|
|
|
15
16
|
get_unique,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
|
-
from ._util import EpicsSignalBackend, get_pv_basename_and_field
|
|
19
|
+
from ._util import EpicsOptions, EpicsSignalBackend, get_pv_basename_and_field
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class EpicsProtocol(Enum):
|
|
@@ -78,14 +79,18 @@ def get_signal_backend_type(protocol: EpicsProtocol) -> type[EpicsSignalBackend]
|
|
|
78
79
|
|
|
79
80
|
|
|
80
81
|
def _epics_signal_backend(
|
|
81
|
-
datatype: type[SignalDatatypeT] | None,
|
|
82
|
+
datatype: type[SignalDatatypeT] | None,
|
|
83
|
+
read_pv: str,
|
|
84
|
+
write_pv: str,
|
|
85
|
+
options: EpicsOptions | None = None,
|
|
82
86
|
) -> SignalBackend[SignalDatatypeT]:
|
|
83
87
|
"""Create an epics signal backend."""
|
|
84
88
|
r_protocol, r_pv = split_protocol_from_pv(read_pv)
|
|
85
89
|
w_protocol, w_pv = split_protocol_from_pv(write_pv)
|
|
86
90
|
protocol = get_unique({read_pv: r_protocol, write_pv: w_protocol}, "protocols")
|
|
91
|
+
|
|
87
92
|
signal_backend_type = get_signal_backend_type(protocol)
|
|
88
|
-
return signal_backend_type(datatype, r_pv, w_pv)
|
|
93
|
+
return signal_backend_type(datatype, r_pv, w_pv, options)
|
|
89
94
|
|
|
90
95
|
|
|
91
96
|
def epics_signal_rw(
|
|
@@ -95,6 +100,7 @@ def epics_signal_rw(
|
|
|
95
100
|
name: str = "",
|
|
96
101
|
timeout: float = DEFAULT_TIMEOUT,
|
|
97
102
|
attempts: int = 1,
|
|
103
|
+
wait: bool | Callable[[SignalDatatypeT], bool] = True,
|
|
98
104
|
) -> SignalRW[SignalDatatypeT]:
|
|
99
105
|
"""Create a `SignalRW` backed by 1 or 2 EPICS PVs.
|
|
100
106
|
|
|
@@ -104,7 +110,9 @@ def epics_signal_rw(
|
|
|
104
110
|
:param name: The name of the signal (defaults to empty string)
|
|
105
111
|
:param timeout: A timeout to be used when reading (not connecting) this signal
|
|
106
112
|
"""
|
|
107
|
-
backend = _epics_signal_backend(
|
|
113
|
+
backend = _epics_signal_backend(
|
|
114
|
+
datatype, read_pv, write_pv or read_pv, EpicsOptions(wait=wait)
|
|
115
|
+
)
|
|
108
116
|
return SignalRW(backend, name=name, timeout=timeout, attempts=attempts)
|
|
109
117
|
|
|
110
118
|
|
|
@@ -115,6 +123,7 @@ def epics_signal_rw_rbv(
|
|
|
115
123
|
name: str = "",
|
|
116
124
|
timeout: float = DEFAULT_TIMEOUT,
|
|
117
125
|
attempts: int = 1,
|
|
126
|
+
wait: bool | Callable[[SignalDatatypeT], bool] = True,
|
|
118
127
|
) -> SignalRW[SignalDatatypeT]:
|
|
119
128
|
"""Create a `SignalRW` backed by 1 or 2 EPICS PVs, with a suffix on the readback pv.
|
|
120
129
|
|
|
@@ -131,7 +140,7 @@ def epics_signal_rw_rbv(
|
|
|
131
140
|
read_pv = f"{write_pv}{read_suffix}"
|
|
132
141
|
|
|
133
142
|
return epics_signal_rw(
|
|
134
|
-
datatype, read_pv, write_pv, name, timeout=timeout, attempts=attempts
|
|
143
|
+
datatype, read_pv, write_pv, name, timeout=timeout, attempts=attempts, wait=wait
|
|
135
144
|
)
|
|
136
145
|
|
|
137
146
|
|
|
@@ -158,6 +167,7 @@ def epics_signal_w(
|
|
|
158
167
|
name: str = "",
|
|
159
168
|
timeout: float = DEFAULT_TIMEOUT,
|
|
160
169
|
attempts: int = 1,
|
|
170
|
+
wait: bool | Callable[[SignalDatatypeT], bool] = True,
|
|
161
171
|
) -> SignalW[SignalDatatypeT]:
|
|
162
172
|
"""Create a `SignalW` backed by 1 EPICS PVs.
|
|
163
173
|
|
|
@@ -166,7 +176,9 @@ def epics_signal_w(
|
|
|
166
176
|
:param name: The name of the signal (defaults to empty string)
|
|
167
177
|
:param timeout: A timeout to be used when reading (not connecting) this signal
|
|
168
178
|
"""
|
|
169
|
-
backend = _epics_signal_backend(
|
|
179
|
+
backend = _epics_signal_backend(
|
|
180
|
+
datatype, write_pv, write_pv, EpicsOptions(wait=wait)
|
|
181
|
+
)
|
|
170
182
|
return SignalW(backend, name=name, timeout=timeout, attempts=attempts)
|
|
171
183
|
|
|
172
184
|
|
ophyd_async/epics/core/_util.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from collections.abc import Mapping, Sequence
|
|
2
|
-
from
|
|
1
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Generic, TypeVar, get_args, get_origin
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
5
6
|
|
|
@@ -19,6 +20,23 @@ from ophyd_async.core import (
|
|
|
19
20
|
T = TypeVar("T")
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
@dataclass
|
|
24
|
+
class EpicsOptions(Generic[SignalDatatypeT]):
|
|
25
|
+
"""Options for EPICS Signals."""
|
|
26
|
+
|
|
27
|
+
wait: bool | Callable[[SignalDatatypeT], bool] = True
|
|
28
|
+
"""Whether to wait for server-side completion of the operation:
|
|
29
|
+
|
|
30
|
+
- `True`: Return when server-side operation has completed
|
|
31
|
+
- `False`: Return when server-side operation has started
|
|
32
|
+
- `callable`: Call with the value being put to decide whether to wait
|
|
33
|
+
|
|
34
|
+
For example, use `EpicsOption(wait=non_zero)` for busy records like
|
|
35
|
+
areaDetector acquire PVs that should not wait when being set to zero
|
|
36
|
+
as it causes a deadlock.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
22
40
|
def get_pv_basename_and_field(pv: str) -> tuple[str, str | None]:
|
|
23
41
|
"""Split PV into record name and field."""
|
|
24
42
|
if "." in pv:
|
|
@@ -75,9 +93,11 @@ class EpicsSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
75
93
|
datatype: type[SignalDatatypeT] | None,
|
|
76
94
|
read_pv: str = "",
|
|
77
95
|
write_pv: str = "",
|
|
96
|
+
options: EpicsOptions | None = None,
|
|
78
97
|
):
|
|
79
98
|
self.read_pv = read_pv
|
|
80
99
|
self.write_pv = write_pv
|
|
100
|
+
self.options = options or EpicsOptions()
|
|
81
101
|
super().__init__(datatype)
|
|
82
102
|
|
|
83
103
|
|
|
@@ -86,5 +106,5 @@ async def stop_busy_record(
|
|
|
86
106
|
value: SignalDatatypeT,
|
|
87
107
|
timeout: float = DEFAULT_TIMEOUT,
|
|
88
108
|
) -> None:
|
|
89
|
-
await signal.set(value
|
|
109
|
+
await signal.set(value)
|
|
90
110
|
await wait_for_value(signal, value, timeout=timeout)
|
ophyd_async/epics/demo/_motor.py
CHANGED
|
@@ -55,8 +55,8 @@ class DemoMotor(EpicsDevice, StandardReadable, Movable, Stoppable):
|
|
|
55
55
|
# If not supplied, calculate a suitable timeout for the move
|
|
56
56
|
if timeout == CALCULATE_TIMEOUT:
|
|
57
57
|
timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
|
|
58
|
-
#
|
|
59
|
-
await self.setpoint.set(new_position
|
|
58
|
+
# Setting the setpoint starts the motion
|
|
59
|
+
await self.setpoint.set(new_position)
|
|
60
60
|
# Observe the readback Signal, and on each new position...
|
|
61
61
|
async for current_position in observe_value(
|
|
62
62
|
self.readback, done_timeout=timeout
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -97,7 +97,7 @@ class InstantMotorMock(DeviceMock["Motor"]):
|
|
|
97
97
|
set_mock_value(device.motor_done_move, 1)
|
|
98
98
|
|
|
99
99
|
# When setpoint is written to, immediately update readback and done flag
|
|
100
|
-
def _instant_move(value
|
|
100
|
+
def _instant_move(value):
|
|
101
101
|
set_mock_value(device.motor_done_move, 0) # Moving
|
|
102
102
|
set_mock_value(device.user_readback, value) # Arrive instantly
|
|
103
103
|
set_mock_value(device.motor_done_move, 1) # Done
|
|
@@ -145,7 +145,9 @@ class Motor(
|
|
|
145
145
|
# Note:cannot use epics_signal_x here, as the motor record specifies that
|
|
146
146
|
# we must write 1 to stop the motor. Simply processing the record is not
|
|
147
147
|
# sufficient.
|
|
148
|
-
|
|
148
|
+
# Put with completion will never complete as we are waiting for completion on
|
|
149
|
+
# the move in set, so need to pass wait=False
|
|
150
|
+
self.motor_stop = epics_signal_w(int, prefix + ".STOP", wait=False)
|
|
149
151
|
|
|
150
152
|
# Whether set() should complete successfully or not
|
|
151
153
|
self._set_success = True
|
|
@@ -283,27 +285,23 @@ class Motor(
|
|
|
283
285
|
|
|
284
286
|
await self.check_motor_limit(old_position, new_position)
|
|
285
287
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
precision=precision,
|
|
297
|
-
)
|
|
288
|
+
async with self.user_setpoint.set(new_position, timeout=timeout):
|
|
289
|
+
async for current_position in observe_value(self.user_readback):
|
|
290
|
+
yield WatcherUpdate(
|
|
291
|
+
current=current_position,
|
|
292
|
+
initial=old_position,
|
|
293
|
+
target=new_position,
|
|
294
|
+
name=self.name,
|
|
295
|
+
unit=units,
|
|
296
|
+
precision=precision,
|
|
297
|
+
)
|
|
298
298
|
if not self._set_success:
|
|
299
299
|
raise RuntimeError("Motor was stopped")
|
|
300
300
|
|
|
301
301
|
async def stop(self, success=False):
|
|
302
302
|
"""Request to stop moving and return immediately."""
|
|
303
303
|
self._set_success = success
|
|
304
|
-
|
|
305
|
-
# the move above, so need to pass wait=False
|
|
306
|
-
await self.motor_stop.set(1, wait=False)
|
|
304
|
+
await self.motor_stop.set(1)
|
|
307
305
|
|
|
308
306
|
async def locate(self) -> Location[float]:
|
|
309
307
|
"""Return the current setpoint and readback of the motor."""
|
|
@@ -163,7 +163,7 @@ class OdinWriter(DetectorWriter):
|
|
|
163
163
|
|
|
164
164
|
async def close(self) -> None:
|
|
165
165
|
await stop_busy_record(self._drv.capture, Writing.DONE, timeout=DEFAULT_TIMEOUT)
|
|
166
|
-
await self._drv.meta_stop.set(True
|
|
166
|
+
await self._drv.meta_stop.set(True)
|
|
167
167
|
if self._capture_status and not self._capture_status.done:
|
|
168
168
|
await self._capture_status
|
|
169
169
|
self._capture_status = None
|
|
@@ -2,7 +2,7 @@ from collections.abc import Sequence
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
|
-
from ophyd_async.core import Array1D, Device, DeviceVector, StandardReadable
|
|
5
|
+
from ophyd_async.core import Array1D, Device, DeviceVector, StandardReadable, SubsetEnum
|
|
6
6
|
from ophyd_async.epics import motor
|
|
7
7
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
8
8
|
|
|
@@ -10,8 +10,16 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal
|
|
|
10
10
|
CS_INDEX = {letter: index + 1 for index, letter in enumerate("ABCUVWXYZ")}
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class PmacExecuteState(SubsetEnum):
|
|
14
|
+
DONE = "Done"
|
|
15
|
+
EXECUTING = "Executing"
|
|
16
|
+
|
|
17
|
+
|
|
13
18
|
class PmacTrajectoryIO(StandardReadable):
|
|
14
|
-
"""Device that moves a PMAC Motor record.
|
|
19
|
+
"""Device that moves a PMAC Motor record.
|
|
20
|
+
|
|
21
|
+
This mirrors the interface provided by pmac/Db/pmacControllerTrajectory.template
|
|
22
|
+
"""
|
|
15
23
|
|
|
16
24
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
17
25
|
self.time_array = epics_signal_rw(
|
|
@@ -45,6 +53,9 @@ class PmacTrajectoryIO(StandardReadable):
|
|
|
45
53
|
# be a SignalRW to be waited on in PmacTrajectoryTriggerLogic.
|
|
46
54
|
# TODO: Change record type to bo from busy (https://github.com/DiamondLightSource/pmac/issues/154)
|
|
47
55
|
self.execute_profile = epics_signal_rw(bool, prefix + "ProfileExecute")
|
|
56
|
+
self.execute_state = epics_signal_r(
|
|
57
|
+
PmacExecuteState, prefix + "ProfileExecuteState_RBV"
|
|
58
|
+
)
|
|
48
59
|
self.abort_profile = epics_signal_x(prefix + "ProfileAbort")
|
|
49
60
|
self.profile_cs_name = epics_signal_rw(str, prefix + "ProfileCsName")
|
|
50
61
|
self.calculate_velocities = epics_signal_rw(bool, prefix + "ProfileCalcVel")
|
|
@@ -56,6 +67,7 @@ class PmacAxisAssignmentIO(Device):
|
|
|
56
67
|
"""A Device that (direct) moves a PMAC Coordinate System Motor.
|
|
57
68
|
|
|
58
69
|
Note that this does not go through a motor record.
|
|
70
|
+
This mirrors the interface provided by pmac/Db/motor_in_cs.template
|
|
59
71
|
"""
|
|
60
72
|
|
|
61
73
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
@@ -66,7 +78,11 @@ class PmacAxisAssignmentIO(Device):
|
|
|
66
78
|
|
|
67
79
|
|
|
68
80
|
class PmacCoordIO(Device):
|
|
69
|
-
"""A Device that represents a Pmac Coordinate System.
|
|
81
|
+
"""A Device that represents a Pmac Coordinate System.
|
|
82
|
+
|
|
83
|
+
This mirrors the interfaces provided by pmac/Db/pmacCsController.template,
|
|
84
|
+
and pmac/Db/pmacDirectMotor.template
|
|
85
|
+
"""
|
|
70
86
|
|
|
71
87
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
72
88
|
self.defer_moves = epics_signal_rw(bool, f"{prefix}DeferMoves")
|
|
@@ -81,7 +97,10 @@ class PmacCoordIO(Device):
|
|
|
81
97
|
|
|
82
98
|
|
|
83
99
|
class PmacIO(Device):
|
|
84
|
-
"""Device that represents a pmac controller.
|
|
100
|
+
"""Device that represents a pmac controller.
|
|
101
|
+
|
|
102
|
+
This mirrors the interface provided by pmac/Db/pmacController.template
|
|
103
|
+
"""
|
|
85
104
|
|
|
86
105
|
def __init__(
|
|
87
106
|
self,
|