ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.0a2__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 +97 -62
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +106 -125
- ophyd_async/core/_device.py +69 -63
- ophyd_async/core/_device_filler.py +65 -1
- ophyd_async/core/_flyer.py +14 -5
- 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 +44 -35
- ophyd_async/core/_settings.py +36 -27
- ophyd_async/core/_signal.py +262 -170
- ophyd_async/core/_signal_backend.py +56 -13
- ophyd_async/core/_soft_signal_backend.py +16 -11
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +41 -11
- ophyd_async/core/_utils.py +96 -49
- ophyd_async/core/_yaml_settings.py +2 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/_andor.py +2 -2
- ophyd_async/epics/adandor/_andor_controller.py +4 -2
- ophyd_async/epics/adandor/_andor_io.py +2 -4
- ophyd_async/epics/adaravis/__init__.py +5 -0
- ophyd_async/epics/adaravis/_aravis.py +4 -8
- ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +23 -8
- ophyd_async/epics/adcore/_core_detector.py +42 -2
- ophyd_async/epics/adcore/_core_io.py +124 -99
- ophyd_async/epics/adcore/_core_logic.py +106 -27
- ophyd_async/epics/adcore/_core_writer.py +12 -8
- ophyd_async/epics/adcore/_hdf_writer.py +21 -38
- ophyd_async/epics/adcore/_single_trigger.py +2 -2
- ophyd_async/epics/adcore/_utils.py +2 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +3 -3
- ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +5 -0
- ophyd_async/epics/adpilatus/_pilatus.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +4 -14
- ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +3 -2
- ophyd_async/epics/advimba/_vimba_controller.py +4 -2
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +35 -16
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +10 -2
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +4 -4
- ophyd_async/epics/demo/__init__.py +16 -0
- 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/{sim/mover.db → demo/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 +65 -28
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/_example_ioc.py +21 -9
- ophyd_async/epics/testing/_utils.py +3 -0
- ophyd_async/epics/testing/test_records.db +8 -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 -6
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +5 -0
- ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
- ophyd_async/fastcs/panda/_table.py +9 -6
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +2 -0
- ophyd_async/plan_stubs/_ensure_connected.py +1 -0
- ophyd_async/plan_stubs/_fly.py +2 -4
- ophyd_async/plan_stubs/_nd_attributes.py +2 -0
- ophyd_async/plan_stubs/_panda.py +1 -0
- ophyd_async/plan_stubs/_settings.py +43 -16
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
- ophyd_async/sim/__init__.py +24 -14
- 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 +18 -32
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +136 -60
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/{sim → demo}/_counter.py +2 -0
- ophyd_async/tango/{sim → demo}/_detector.py +2 -0
- ophyd_async/tango/{sim → demo}/_mover.py +5 -4
- ophyd_async/tango/{sim → 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 +29 -7
- ophyd_async/testing/_assert.py +145 -83
- ophyd_async/testing/_mock_signal_utils.py +56 -70
- ophyd_async/testing/_one_of_everything.py +41 -21
- ophyd_async/testing/_single_derived.py +89 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/sim/__init__.py +0 -54
- ophyd_async/epics/sim/_ioc.py +0 -29
- ophyd_async/epics/sim/_mover.py +0 -101
- ophyd_async/epics/sim/_sensor.py +0 -37
- ophyd_async/epics/sim/sensor.db +0 -19
- ophyd_async/sim/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
- ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
- ophyd_async/sim/_sim_motor.py +0 -107
- ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
- /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
- /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/top_level.txt +0 -0
ophyd_async/epics/core/_aioca.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import sys
|
|
3
|
+
import typing
|
|
3
4
|
from collections.abc import Sequence
|
|
5
|
+
from functools import cache
|
|
4
6
|
from math import isnan, nan
|
|
5
7
|
from typing import Any, Generic, cast
|
|
6
8
|
|
|
@@ -47,6 +49,7 @@ def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
|
47
49
|
low=None if isnan(low) else low,
|
|
48
50
|
high=None if isnan(high) else high,
|
|
49
51
|
)
|
|
52
|
+
return None
|
|
50
53
|
|
|
51
54
|
limits = Limits()
|
|
52
55
|
if limits_range := get_limits("alarm"):
|
|
@@ -181,6 +184,9 @@ def make_converter(
|
|
|
181
184
|
Dbr, get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
|
|
182
185
|
)
|
|
183
186
|
is_array = bool([v for v in values.values() if v.element_count > 1])
|
|
187
|
+
# Make the datatype canonical for the inference below
|
|
188
|
+
if datatype == typing.Sequence[str]:
|
|
189
|
+
datatype = Sequence[str]
|
|
184
190
|
# Infer a datatype and converter from the dbr
|
|
185
191
|
inferred_datatype, converter_cls = _datatype_converter_from_dbr[(pv_dbr, is_array)]
|
|
186
192
|
# Some override cases
|
|
@@ -227,19 +233,18 @@ def make_converter(
|
|
|
227
233
|
)
|
|
228
234
|
|
|
229
235
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
# Cached call to avoid repeated initialization attempts
|
|
237
|
+
@cache
|
|
233
238
|
def _use_pyepics_context_if_imported():
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
ca.use_initial_context()
|
|
239
|
-
_tried_pyepics = True
|
|
239
|
+
"""Sets up the pyepics context if the module is imported."""
|
|
240
|
+
ca = sys.modules.get("epics.ca", None)
|
|
241
|
+
if ca:
|
|
242
|
+
ca.use_initial_context()
|
|
240
243
|
|
|
241
244
|
|
|
242
245
|
class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
246
|
+
"""Backend for a signal to interact with PVs over channel access."""
|
|
247
|
+
|
|
243
248
|
def __init__(
|
|
244
249
|
self,
|
|
245
250
|
datatype: type[SignalDatatypeT] | None,
|
|
@@ -293,13 +298,27 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
293
298
|
write_value = self.initial_values[self.write_pv]
|
|
294
299
|
else:
|
|
295
300
|
write_value = self.converter.write_value(value)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
try:
|
|
302
|
+
await caput(
|
|
303
|
+
self.write_pv,
|
|
304
|
+
write_value,
|
|
305
|
+
datatype=self.converter.write_dbr,
|
|
306
|
+
wait=wait,
|
|
307
|
+
timeout=None,
|
|
308
|
+
)
|
|
309
|
+
except CANothing as exc:
|
|
310
|
+
# If we ran into a write error, check to see if there is a list
|
|
311
|
+
# of valid choices, and if the value we tried to write is in that list.
|
|
312
|
+
valid_choices = self.converter.metadata.get("choices")
|
|
313
|
+
if valid_choices:
|
|
314
|
+
if value not in valid_choices:
|
|
315
|
+
msg = (
|
|
316
|
+
f"{value} is not a valid choice for {self.write_pv}, "
|
|
317
|
+
f"valid choices: {self.converter.metadata.get('choices')}"
|
|
318
|
+
)
|
|
319
|
+
raise ValueError(msg) from exc
|
|
320
|
+
raise
|
|
321
|
+
raise
|
|
303
322
|
|
|
304
323
|
async def get_datakey(self, source: str) -> DataKey:
|
|
305
324
|
value = await self._caget(self.read_pv, FORMAT_CTRL)
|
|
@@ -10,6 +10,8 @@ from ._signal import EpicsSignalBackend, get_signal_backend_type, split_protocol
|
|
|
10
10
|
|
|
11
11
|
@dataclass
|
|
12
12
|
class PvSuffix:
|
|
13
|
+
"""Define the PV suffix to be appended to the device prefix."""
|
|
14
|
+
|
|
13
15
|
read_suffix: str
|
|
14
16
|
write_suffix: str | None = None
|
|
15
17
|
|
|
@@ -36,6 +38,8 @@ def fill_backend_with_prefix(
|
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
class EpicsDeviceConnector(DeviceConnector):
|
|
41
|
+
"""Used for connecting signals to static EPICS pvs."""
|
|
42
|
+
|
|
39
43
|
def __init__(self, prefix: str) -> None:
|
|
40
44
|
self.prefix = prefix
|
|
41
45
|
|
|
@@ -5,6 +5,8 @@ from ._pvi_connector import PviDeviceConnector
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class EpicsDevice(Device):
|
|
8
|
+
"""Baseclass to allow child signals to be created declaratively."""
|
|
9
|
+
|
|
8
10
|
def __init__(self, prefix: str, with_pvi: bool = False, name: str = ""):
|
|
9
11
|
if with_pvi:
|
|
10
12
|
connector = PviDeviceConnector(prefix)
|
ophyd_async/epics/core/_p4p.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import atexit
|
|
5
5
|
import logging
|
|
6
|
+
import typing
|
|
6
7
|
from collections.abc import Mapping, Sequence
|
|
7
8
|
from math import isnan, nan
|
|
8
9
|
from typing import Any, Generic
|
|
@@ -46,6 +47,7 @@ def _limits_from_value(value: Any) -> Limits:
|
|
|
46
47
|
low=None if isnan(low) else low,
|
|
47
48
|
high=None if isnan(high) else high,
|
|
48
49
|
)
|
|
50
|
+
return None
|
|
49
51
|
|
|
50
52
|
limits = Limits()
|
|
51
53
|
if limits_range := get_limits("valueAlarm", "lowAlarmLimit", "highAlarmLimit"):
|
|
@@ -245,6 +247,9 @@ def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverte
|
|
|
245
247
|
{k: _get_specifier(v) for k, v in values.items()},
|
|
246
248
|
"value type specifiers",
|
|
247
249
|
)
|
|
250
|
+
# Make the datatype canonical for the inference below
|
|
251
|
+
if datatype == typing.Sequence[str]:
|
|
252
|
+
datatype = Sequence[str]
|
|
248
253
|
# Infer a datatype and converter from the typeid and specifier
|
|
249
254
|
inferred_datatype, converter_cls = _datatype_converter_from_typeid[
|
|
250
255
|
(typeid, specifier)
|
|
@@ -328,13 +333,16 @@ async def pvget_with_timeout(pv: str, timeout: float) -> Any:
|
|
|
328
333
|
|
|
329
334
|
|
|
330
335
|
def _pva_request_string(fields: Sequence[str]) -> str:
|
|
331
|
-
"""
|
|
332
|
-
|
|
336
|
+
"""Convert a list of requested fields into a PVA request string.
|
|
337
|
+
|
|
338
|
+
This can be passed to p4p.
|
|
333
339
|
"""
|
|
334
340
|
return f"field({','.join(fields)})"
|
|
335
341
|
|
|
336
342
|
|
|
337
343
|
class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
344
|
+
"""Backend for a signal to interact with PVs over pva."""
|
|
345
|
+
|
|
338
346
|
def __init__(
|
|
339
347
|
self,
|
|
340
348
|
datatype: type[SignalDatatypeT] | None,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import Literal, cast
|
|
4
|
+
|
|
3
5
|
from ophyd_async.core import (
|
|
4
6
|
Device,
|
|
5
7
|
DeviceConnector,
|
|
@@ -7,6 +9,7 @@ from ophyd_async.core import (
|
|
|
7
9
|
Signal,
|
|
8
10
|
SignalR,
|
|
9
11
|
SignalRW,
|
|
12
|
+
SignalW,
|
|
10
13
|
SignalX,
|
|
11
14
|
)
|
|
12
15
|
from ophyd_async.core._utils import LazyMock
|
|
@@ -16,6 +19,27 @@ from ._signal import PvaSignalBackend, pvget_with_timeout
|
|
|
16
19
|
|
|
17
20
|
Entry = dict[str, str]
|
|
18
21
|
|
|
22
|
+
OldPVIVector = list[Entry | None]
|
|
23
|
+
# The older PVI structure has vectors of the form
|
|
24
|
+
# structure[] ttlout
|
|
25
|
+
# (none)
|
|
26
|
+
# structure
|
|
27
|
+
# string d PANDABLOCKS_IOC:TTLOUT1:PVI
|
|
28
|
+
# structure
|
|
29
|
+
# string d PANDABLOCKS_IOC:TTLOUT2:PVI
|
|
30
|
+
# structure
|
|
31
|
+
# string d PANDABLOCKS_IOC:TTLOUT3:PVI
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
FastCSPVIVector = dict[Literal["d"], Entry]
|
|
35
|
+
# The newer pva FastCS PVI structure has vectors of the form
|
|
36
|
+
# structure ttlout
|
|
37
|
+
# structure d
|
|
38
|
+
# string v1 FASTCS_PANDA:Ttlout1:PVI
|
|
39
|
+
# string v2 FASTCS_PANDA:Ttlout2:PVI
|
|
40
|
+
# string v3 FASTCS_PANDA:Ttlout3:PVI
|
|
41
|
+
# string v4 FASTCS_PANDA:Ttlout4:PVI
|
|
42
|
+
|
|
19
43
|
|
|
20
44
|
def _get_signal_details(entry: Entry) -> tuple[type[Signal], str, str]:
|
|
21
45
|
match entry:
|
|
@@ -23,6 +47,8 @@ def _get_signal_details(entry: Entry) -> tuple[type[Signal], str, str]:
|
|
|
23
47
|
return SignalR, read_pv, read_pv
|
|
24
48
|
case {"r": read_pv, "w": write_pv}:
|
|
25
49
|
return SignalRW, read_pv, write_pv
|
|
50
|
+
case {"w": write_pv}:
|
|
51
|
+
return SignalW, write_pv, write_pv
|
|
26
52
|
case {"rw": read_write_pv}:
|
|
27
53
|
return SignalRW, read_write_pv, read_write_pv
|
|
28
54
|
case {"x": execute_pv}:
|
|
@@ -31,7 +57,26 @@ def _get_signal_details(entry: Entry) -> tuple[type[Signal], str, str]:
|
|
|
31
57
|
raise TypeError(f"Can't process entry {entry}")
|
|
32
58
|
|
|
33
59
|
|
|
60
|
+
def _is_device_vector_entry(entry: Entry | OldPVIVector | FastCSPVIVector) -> bool:
|
|
61
|
+
return isinstance(entry, list) or (
|
|
62
|
+
entry.keys() == {"d"} and isinstance(entry["d"], dict)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
34
66
|
class PviDeviceConnector(DeviceConnector):
|
|
67
|
+
"""Connect to PVI structure served over PVA.
|
|
68
|
+
|
|
69
|
+
At init, fill in all the type hinted signals. At connection check their
|
|
70
|
+
types and fill in any extra signals.
|
|
71
|
+
|
|
72
|
+
:param prefix:
|
|
73
|
+
The PV prefix of the device, "PVI" will be appended to it to get the PVI
|
|
74
|
+
PV.
|
|
75
|
+
:param error_hint:
|
|
76
|
+
If given, this will be appended to the error message if any of they type
|
|
77
|
+
hinted Signals are not present.
|
|
78
|
+
"""
|
|
79
|
+
|
|
35
80
|
def __init__(self, prefix: str = "", error_hint: str = "") -> None:
|
|
36
81
|
# TODO: what happens if we get a leading "pva://" here?
|
|
37
82
|
self.prefix = prefix
|
|
@@ -70,21 +115,33 @@ class PviDeviceConnector(DeviceConnector):
|
|
|
70
115
|
device.set_name(device.name)
|
|
71
116
|
return await super().connect_mock(device, mock)
|
|
72
117
|
|
|
118
|
+
def _fill_vector_child(self, name: str, entry: OldPVIVector | FastCSPVIVector):
|
|
119
|
+
if isinstance(entry, list):
|
|
120
|
+
for i, e in enumerate(entry):
|
|
121
|
+
if e:
|
|
122
|
+
self._fill_child(name, e, i)
|
|
123
|
+
else:
|
|
124
|
+
for i_string, e in entry["d"].items():
|
|
125
|
+
self._fill_child(name, {"d": e}, int(i_string.lstrip("v")))
|
|
126
|
+
|
|
73
127
|
async def connect_real(
|
|
74
128
|
self, device: Device, timeout: float, force_reconnect: bool
|
|
75
129
|
) -> None:
|
|
76
130
|
pvi_structure = await pvget_with_timeout(self.pvi_pv, timeout)
|
|
77
|
-
|
|
131
|
+
|
|
132
|
+
entries: dict[str, Entry | OldPVIVector | FastCSPVIVector] = pvi_structure[
|
|
133
|
+
"value"
|
|
134
|
+
].todict()
|
|
78
135
|
# Fill based on what PVI gives us
|
|
79
136
|
for name, entry in entries.items():
|
|
80
|
-
if
|
|
81
|
-
|
|
82
|
-
|
|
137
|
+
if _is_device_vector_entry(entry):
|
|
138
|
+
self._fill_vector_child(
|
|
139
|
+
name, cast(OldPVIVector | FastCSPVIVector, entry)
|
|
140
|
+
)
|
|
83
141
|
else:
|
|
84
|
-
# This is a
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
self._fill_child(name, e, i)
|
|
142
|
+
# This is a child
|
|
143
|
+
self._fill_child(name, cast(Entry, entry))
|
|
144
|
+
|
|
88
145
|
# Check that all the requested children have been filled
|
|
89
146
|
suffix = f"\n{self.error_hint}" if self.error_hint else ""
|
|
90
147
|
self.filler.check_filled(f"{self.pvi_pv}: {entries}{suffix}")
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""EPICS Signals over CA or PVA"""
|
|
1
|
+
"""EPICS Signals over CA or PVA."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
|
|
7
7
|
from ophyd_async.core import (
|
|
8
|
+
DEFAULT_TIMEOUT,
|
|
8
9
|
SignalBackend,
|
|
9
10
|
SignalDatatypeT,
|
|
10
11
|
SignalR,
|
|
@@ -73,6 +74,7 @@ def get_signal_backend_type(protocol: EpicsProtocol) -> type[EpicsSignalBackend]
|
|
|
73
74
|
return CaSignalBackend
|
|
74
75
|
case EpicsProtocol.PVA:
|
|
75
76
|
return PvaSignalBackend
|
|
77
|
+
raise TypeError(f"Unsupported protocol: {protocol}")
|
|
76
78
|
|
|
77
79
|
|
|
78
80
|
def _epics_signal_backend(
|
|
@@ -91,20 +93,18 @@ def epics_signal_rw(
|
|
|
91
93
|
read_pv: str,
|
|
92
94
|
write_pv: str | None = None,
|
|
93
95
|
name: str = "",
|
|
96
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
94
97
|
) -> SignalRW[SignalDatatypeT]:
|
|
95
|
-
"""Create a `SignalRW` backed by 1 or 2 EPICS PVs
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
The PV to read and monitor
|
|
103
|
-
write_pv:
|
|
104
|
-
If given, use this PV to write to, otherwise use read_pv
|
|
98
|
+
"""Create a `SignalRW` backed by 1 or 2 EPICS PVs.
|
|
99
|
+
|
|
100
|
+
:param datatype: Check that the PV is of this type
|
|
101
|
+
:param read_pv: The PV to read and monitor
|
|
102
|
+
:param write_pv: If given, use this PV to write to, otherwise use read_pv
|
|
103
|
+
:param name: The name of the signal (defaults to empty string)
|
|
104
|
+
:param timeout: A timeout to be used when reading (not connecting) this signal
|
|
105
105
|
"""
|
|
106
106
|
backend = _epics_signal_backend(datatype, read_pv, write_pv or read_pv)
|
|
107
|
-
return SignalRW(backend, name=name)
|
|
107
|
+
return SignalRW(backend, name=name, timeout=timeout)
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
def epics_signal_rw_rbv(
|
|
@@ -112,67 +112,67 @@ def epics_signal_rw_rbv(
|
|
|
112
112
|
write_pv: str,
|
|
113
113
|
read_suffix: str = "_RBV",
|
|
114
114
|
name: str = "",
|
|
115
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
115
116
|
) -> SignalRW[SignalDatatypeT]:
|
|
116
|
-
"""Create a `SignalRW` backed by 1 or 2 EPICS PVs, with a suffix on the readback pv
|
|
117
|
-
|
|
118
|
-
Parameters
|
|
119
|
-
----------
|
|
120
|
-
datatype:
|
|
121
|
-
Check that the PV is of this type
|
|
122
|
-
write_pv:
|
|
123
|
-
The PV to write to
|
|
124
|
-
read_suffix:
|
|
125
|
-
Append this suffix to the write pv to create the readback pv
|
|
126
|
-
"""
|
|
117
|
+
"""Create a `SignalRW` backed by 1 or 2 EPICS PVs, with a suffix on the readback pv.
|
|
127
118
|
|
|
119
|
+
:param datatype: Check that the PV is of this type
|
|
120
|
+
:param write_pv: The PV to write to
|
|
121
|
+
:param read_suffix: Append this suffix to the write pv to create the readback pv
|
|
122
|
+
:param name: The name of the signal (defaults to empty string)
|
|
123
|
+
:param timeout: A timeout to be used when reading (not connecting) this signal
|
|
124
|
+
"""
|
|
128
125
|
base_pv, field = get_pv_basename_and_field(write_pv)
|
|
129
126
|
if field is not None:
|
|
130
127
|
read_pv = f"{base_pv}{read_suffix}.{field}"
|
|
131
128
|
else:
|
|
132
129
|
read_pv = f"{write_pv}{read_suffix}"
|
|
133
130
|
|
|
134
|
-
return epics_signal_rw(datatype, read_pv, write_pv, name)
|
|
131
|
+
return epics_signal_rw(datatype, read_pv, write_pv, name, timeout=timeout)
|
|
135
132
|
|
|
136
133
|
|
|
137
134
|
def epics_signal_r(
|
|
138
|
-
datatype: type[SignalDatatypeT],
|
|
135
|
+
datatype: type[SignalDatatypeT],
|
|
136
|
+
read_pv: str,
|
|
137
|
+
name: str = "",
|
|
138
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
139
139
|
) -> SignalR[SignalDatatypeT]:
|
|
140
|
-
"""Create a `SignalR` backed by 1 EPICS PV
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
read_pv:
|
|
147
|
-
The PV to read and monitor
|
|
140
|
+
"""Create a `SignalR` backed by 1 EPICS PV.
|
|
141
|
+
|
|
142
|
+
:param datatype: Check that the PV is of this type
|
|
143
|
+
:param read_pv: The PV to read from
|
|
144
|
+
:param name: The name of the signal (defaults to empty string)
|
|
145
|
+
:param timeout: A timeout to be used when reading (not connecting) this signal
|
|
148
146
|
"""
|
|
149
147
|
backend = _epics_signal_backend(datatype, read_pv, read_pv)
|
|
150
|
-
return SignalR(backend, name=name)
|
|
148
|
+
return SignalR(backend, name=name, timeout=timeout)
|
|
151
149
|
|
|
152
150
|
|
|
153
151
|
def epics_signal_w(
|
|
154
|
-
datatype: type[SignalDatatypeT],
|
|
152
|
+
datatype: type[SignalDatatypeT],
|
|
153
|
+
write_pv: str,
|
|
154
|
+
name: str = "",
|
|
155
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
155
156
|
) -> SignalW[SignalDatatypeT]:
|
|
156
|
-
"""Create a `SignalW` backed by 1 EPICS PVs
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
write_pv:
|
|
163
|
-
The PV to write to
|
|
157
|
+
"""Create a `SignalW` backed by 1 EPICS PVs.
|
|
158
|
+
|
|
159
|
+
:param datatype: Check that the PV is of this type
|
|
160
|
+
:param write_pv: The PV to write to
|
|
161
|
+
:param name: The name of the signal (defaults to empty string)
|
|
162
|
+
:param timeout: A timeout to be used when reading (not connecting) this signal
|
|
164
163
|
"""
|
|
165
164
|
backend = _epics_signal_backend(datatype, write_pv, write_pv)
|
|
166
|
-
return SignalW(backend, name=name)
|
|
165
|
+
return SignalW(backend, name=name, timeout=timeout)
|
|
167
166
|
|
|
168
167
|
|
|
169
|
-
def epics_signal_x(
|
|
170
|
-
|
|
168
|
+
def epics_signal_x(
|
|
169
|
+
write_pv: str, name: str = "", timeout: float = DEFAULT_TIMEOUT
|
|
170
|
+
) -> SignalX:
|
|
171
|
+
"""Create a `SignalX` backed by 1 EPICS PVs.
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
The PV to write its initial value to on trigger
|
|
173
|
+
:param write_pv: The PV to write its initial value to on trigger
|
|
174
|
+
:param name: The name of the signal
|
|
175
|
+
:param timeout: A timeout to be used when reading (not connecting) this signal
|
|
176
176
|
"""
|
|
177
177
|
backend = _epics_signal_backend(None, write_pv, write_pv)
|
|
178
|
-
return SignalX(backend, name=name)
|
|
178
|
+
return SignalX(backend, name=name, timeout=timeout)
|
ophyd_async/epics/core/_util.py
CHANGED
|
@@ -13,12 +13,12 @@ from ophyd_async.core import (
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def get_pv_basename_and_field(pv: str) -> tuple[str, str | None]:
|
|
16
|
-
"""
|
|
17
|
-
|
|
16
|
+
"""Split PV into record name and field."""
|
|
18
17
|
if "." in pv:
|
|
19
|
-
|
|
18
|
+
record, field = pv.split(".", maxsplit=1)
|
|
20
19
|
else:
|
|
21
|
-
|
|
20
|
+
record, field = pv, None
|
|
21
|
+
return (record, field)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def get_supported_values(
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Demo EPICS Devices for the tutorial."""
|
|
2
|
+
|
|
3
|
+
from ._ioc import start_ioc_subprocess
|
|
4
|
+
from ._motor import DemoMotor
|
|
5
|
+
from ._point_detector import DemoPointDetector
|
|
6
|
+
from ._point_detector_channel import DemoPointDetectorChannel, EnergyMode
|
|
7
|
+
from ._stage import DemoStage
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DemoMotor",
|
|
11
|
+
"DemoStage",
|
|
12
|
+
"EnergyMode",
|
|
13
|
+
"DemoPointDetectorChannel",
|
|
14
|
+
"DemoPointDetector",
|
|
15
|
+
"start_ioc_subprocess",
|
|
16
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Used for tutorial `Implementing Devices`."""
|
|
2
|
+
|
|
3
|
+
# Import bluesky and ophyd
|
|
4
|
+
import bluesky.plan_stubs as bps # noqa: F401
|
|
5
|
+
import bluesky.plans as bp # noqa: F401
|
|
6
|
+
from bluesky.callbacks.best_effort import BestEffortCallback
|
|
7
|
+
from bluesky.run_engine import RunEngine, autoawait_in_bluesky_event_loop
|
|
8
|
+
|
|
9
|
+
from ophyd_async.core import init_devices
|
|
10
|
+
from ophyd_async.epics import demo, testing
|
|
11
|
+
|
|
12
|
+
# Create a run engine and make ipython use it for `await` commands
|
|
13
|
+
RE = RunEngine(call_returns_result=True)
|
|
14
|
+
autoawait_in_bluesky_event_loop()
|
|
15
|
+
|
|
16
|
+
# Add a callback for plotting
|
|
17
|
+
bec = BestEffortCallback()
|
|
18
|
+
RE.subscribe(bec)
|
|
19
|
+
|
|
20
|
+
# Start IOC with demo pvs in subprocess
|
|
21
|
+
prefix = testing.generate_random_pv_prefix()
|
|
22
|
+
ioc = demo.start_ioc_subprocess(prefix, num_channels=3)
|
|
23
|
+
|
|
24
|
+
# All Devices created within this block will be
|
|
25
|
+
# connected and named at the end of the with block
|
|
26
|
+
with init_devices():
|
|
27
|
+
# Create a sample stage with X and Y motors
|
|
28
|
+
stage = demo.DemoStage(f"{prefix}STAGE:")
|
|
29
|
+
# Create a multi channel counter with the same number
|
|
30
|
+
# of counters as the IOC
|
|
31
|
+
pdet = demo.DemoPointDetector(f"{prefix}DET:", num_channels=3)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ophyd_async.epics.testing import TestingIOC
|
|
5
|
+
|
|
6
|
+
HERE = Path(__file__).absolute().parent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def start_ioc_subprocess(prefix: str, num_channels: int) -> TestingIOC:
|
|
10
|
+
"""Start an IOC subprocess for sample stage and sensor.
|
|
11
|
+
|
|
12
|
+
:param prefix: The prefix for the IOC PVs.
|
|
13
|
+
:param num_channels: The number of point detector channels to create.
|
|
14
|
+
"""
|
|
15
|
+
ioc = TestingIOC()
|
|
16
|
+
# Create X and Y motors
|
|
17
|
+
for suffix in ["X", "Y"]:
|
|
18
|
+
ioc.add_database(HERE / "motor.db", P=f"{prefix}STAGE:{suffix}:")
|
|
19
|
+
# Create a multichannel counter with num_counters
|
|
20
|
+
ioc.add_database(HERE / "point_detector.db", P=f"{prefix}DET:")
|
|
21
|
+
for i in range(1, num_channels + 1):
|
|
22
|
+
ioc.add_database(
|
|
23
|
+
HERE / "point_detector_channel.db",
|
|
24
|
+
P=f"{prefix}DET:",
|
|
25
|
+
CHANNEL=str(i),
|
|
26
|
+
X=f"{prefix}STAGE:X:",
|
|
27
|
+
Y=f"{prefix}STAGE:Y:",
|
|
28
|
+
)
|
|
29
|
+
# Start IOC and register it to be stopped at exit
|
|
30
|
+
ioc.start()
|
|
31
|
+
atexit.register(ioc.stop)
|
|
32
|
+
return ioc
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Annotated as A
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from bluesky.protocols import Movable, Stoppable
|
|
6
|
+
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
CALCULATE_TIMEOUT,
|
|
9
|
+
DEFAULT_TIMEOUT,
|
|
10
|
+
CalculatableTimeout,
|
|
11
|
+
SignalR,
|
|
12
|
+
SignalRW,
|
|
13
|
+
SignalX,
|
|
14
|
+
StandardReadable,
|
|
15
|
+
WatchableAsyncStatus,
|
|
16
|
+
WatcherUpdate,
|
|
17
|
+
observe_value,
|
|
18
|
+
)
|
|
19
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
20
|
+
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DemoMotor(EpicsDevice, StandardReadable, Movable, Stoppable):
|
|
24
|
+
"""A demo movable that moves based on velocity."""
|
|
25
|
+
|
|
26
|
+
# Whether set() should complete successfully or not
|
|
27
|
+
_set_success = True
|
|
28
|
+
# Define some signals
|
|
29
|
+
readback: A[SignalR[float], PvSuffix("Readback"), Format.HINTED_SIGNAL]
|
|
30
|
+
velocity: A[SignalRW[float], PvSuffix("Velocity"), Format.CONFIG_SIGNAL]
|
|
31
|
+
units: A[SignalR[str], PvSuffix("Readback.EGU"), Format.CONFIG_SIGNAL]
|
|
32
|
+
setpoint: A[SignalRW[float], PvSuffix("Setpoint")]
|
|
33
|
+
precision: A[SignalR[int], PvSuffix("Readback.PREC")]
|
|
34
|
+
# If a signal name clashes with a bluesky verb add _ to the attribute name
|
|
35
|
+
stop_: A[SignalX, PvSuffix("Stop.PROC")]
|
|
36
|
+
|
|
37
|
+
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
38
|
+
super().set_name(name, child_name_separator=child_name_separator)
|
|
39
|
+
# Readback should be named the same as its parent in read()
|
|
40
|
+
self.readback.set_name(name)
|
|
41
|
+
|
|
42
|
+
@WatchableAsyncStatus.wrap
|
|
43
|
+
async def set( # type: ignore
|
|
44
|
+
self, new_position: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
|
|
45
|
+
):
|
|
46
|
+
# The move should complete successfully unless stop(success=False) is called
|
|
47
|
+
self._set_success = True
|
|
48
|
+
# Get some variables for the progress bar reporting
|
|
49
|
+
old_position, units, precision, velocity = await asyncio.gather(
|
|
50
|
+
self.setpoint.get_value(),
|
|
51
|
+
self.units.get_value(),
|
|
52
|
+
self.precision.get_value(),
|
|
53
|
+
self.velocity.get_value(),
|
|
54
|
+
)
|
|
55
|
+
# If not supplied, calculate a suitable timeout for the move
|
|
56
|
+
if timeout == CALCULATE_TIMEOUT:
|
|
57
|
+
timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
|
|
58
|
+
# Wait for the value to set, but don't wait for put completion callback
|
|
59
|
+
await self.setpoint.set(new_position, wait=False)
|
|
60
|
+
# Observe the readback Signal, and on each new position...
|
|
61
|
+
async for current_position in observe_value(
|
|
62
|
+
self.readback, done_timeout=timeout
|
|
63
|
+
):
|
|
64
|
+
# Emit a progress bar update
|
|
65
|
+
yield WatcherUpdate(
|
|
66
|
+
current=current_position,
|
|
67
|
+
initial=old_position,
|
|
68
|
+
target=new_position,
|
|
69
|
+
name=self.name,
|
|
70
|
+
unit=units,
|
|
71
|
+
precision=precision,
|
|
72
|
+
)
|
|
73
|
+
# If we are at the desired position the break
|
|
74
|
+
if np.isclose(current_position, new_position):
|
|
75
|
+
break
|
|
76
|
+
# If we were told to stop and report an error then do so
|
|
77
|
+
if not self._set_success:
|
|
78
|
+
raise RuntimeError("Motor was stopped")
|
|
79
|
+
|
|
80
|
+
async def stop(self, success=True):
|
|
81
|
+
self._set_success = success
|
|
82
|
+
await self.stop_.trigger()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Annotated as A
|
|
2
|
+
|
|
3
|
+
from bluesky.protocols import Triggerable
|
|
4
|
+
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
DEFAULT_TIMEOUT,
|
|
7
|
+
AsyncStatus,
|
|
8
|
+
DeviceVector,
|
|
9
|
+
SignalR,
|
|
10
|
+
SignalRW,
|
|
11
|
+
SignalX,
|
|
12
|
+
StandardReadable,
|
|
13
|
+
)
|
|
14
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
15
|
+
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
16
|
+
|
|
17
|
+
from ._point_detector_channel import DemoPointDetectorChannel
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DemoPointDetector(StandardReadable, EpicsDevice, Triggerable):
|
|
21
|
+
"""A demo detector that produces a point values based on X and Y motors."""
|
|
22
|
+
|
|
23
|
+
acquire_time: A[SignalRW[float], PvSuffix("AcquireTime"), Format.CONFIG_SIGNAL]
|
|
24
|
+
start: A[SignalX, PvSuffix("Start.PROC")]
|
|
25
|
+
acquiring: A[SignalR[bool], PvSuffix("Acquiring")]
|
|
26
|
+
reset: A[SignalX, PvSuffix("Reset.PROC")]
|
|
27
|
+
|
|
28
|
+
def __init__(self, prefix: str, num_channels: int = 3, name: str = "") -> None:
|
|
29
|
+
with self.add_children_as_readables():
|
|
30
|
+
self.channel = DeviceVector(
|
|
31
|
+
{
|
|
32
|
+
i: DemoPointDetectorChannel(f"{prefix}{i}:")
|
|
33
|
+
for i in range(1, num_channels + 1)
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
super().__init__(prefix=prefix, name=name)
|
|
37
|
+
|
|
38
|
+
@AsyncStatus.wrap
|
|
39
|
+
async def trigger(self):
|
|
40
|
+
await self.reset.trigger()
|
|
41
|
+
timeout = await self.acquire_time.get_value() + DEFAULT_TIMEOUT
|
|
42
|
+
await self.start.trigger(timeout=timeout)
|