ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/__init__.py +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +102 -74
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +158 -153
- ophyd_async/core/_device.py +143 -115
- ophyd_async/core/_device_filler.py +82 -9
- ophyd_async/core/_flyer.py +16 -7
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +74 -58
- ophyd_async/core/_settings.py +113 -0
- ophyd_async/core/_signal.py +304 -174
- ophyd_async/core/_signal_backend.py +60 -14
- ophyd_async/core/_soft_signal_backend.py +18 -12
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +54 -17
- ophyd_async/core/_utils.py +101 -52
- ophyd_async/core/_yaml_settings.py +66 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +51 -0
- ophyd_async/epics/adandor/_andor_io.py +34 -0
- ophyd_async/epics/adaravis/__init__.py +8 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -41
- ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +36 -14
- ophyd_async/epics/adcore/_core_detector.py +81 -0
- ophyd_async/epics/adcore/_core_io.py +145 -95
- ophyd_async/epics/adcore/_core_logic.py +179 -88
- ophyd_async/epics/adcore/_core_writer.py +223 -0
- ophyd_async/epics/adcore/_hdf_writer.py +51 -92
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +6 -5
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +3 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +32 -27
- ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +7 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +22 -16
- ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +26 -25
- ophyd_async/epics/advimba/_vimba_controller.py +12 -24
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +66 -30
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +50 -18
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +5 -5
- ophyd_async/epics/demo/__init__.py +11 -49
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +83 -38
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +68 -73
- ophyd_async/epics/testing/_utils.py +19 -44
- ophyd_async/epics/testing/test_records.db +16 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -8
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +12 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
- ophyd_async/fastcs/panda/_table.py +13 -7
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +16 -0
- ophyd_async/plan_stubs/_ensure_connected.py +12 -17
- ophyd_async/plan_stubs/_fly.py +3 -5
- ophyd_async/plan_stubs/_nd_attributes.py +9 -5
- ophyd_async/plan_stubs/_panda.py +14 -0
- ophyd_async/plan_stubs/_settings.py +152 -0
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +29 -0
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +21 -33
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +148 -74
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/demo/_counter.py +2 -0
- ophyd_async/tango/demo/_detector.py +2 -0
- ophyd_async/tango/demo/_mover.py +10 -6
- ophyd_async/tango/demo/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +48 -7
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +200 -96
- ophyd_async/testing/_mock_signal_utils.py +59 -73
- ophyd_async/testing/_one_of_everything.py +146 -0
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/demo/_mover.py +0 -95
- ophyd_async/epics/demo/_sensor.py +0 -37
- ophyd_async/epics/demo/sensor.db +0 -19
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
- ophyd_async/sim/demo/_sim_motor.py +0 -107
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
- ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
ophyd_async/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
|
|
|
@@ -35,16 +37,19 @@ from ophyd_async.core import (
|
|
|
35
37
|
|
|
36
38
|
from ._util import EpicsSignalBackend, format_datatype, get_supported_values
|
|
37
39
|
|
|
40
|
+
logger = logging.getLogger("ophyd_async")
|
|
41
|
+
|
|
38
42
|
|
|
39
43
|
def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
40
44
|
def get_limits(limit: str) -> LimitsRange | None:
|
|
41
45
|
low = getattr(value, f"lower_{limit}_limit", nan)
|
|
42
46
|
high = getattr(value, f"upper_{limit}_limit", nan)
|
|
43
|
-
if not (isnan(low) and isnan(high)):
|
|
47
|
+
if not (isnan(low) and isnan(high)) and not high == low == 0:
|
|
44
48
|
return LimitsRange(
|
|
45
49
|
low=None if isnan(low) else low,
|
|
46
50
|
high=None if isnan(high) else high,
|
|
47
51
|
)
|
|
52
|
+
return None
|
|
48
53
|
|
|
49
54
|
limits = Limits()
|
|
50
55
|
if limits_range := get_limits("alarm"):
|
|
@@ -59,14 +64,20 @@ def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
|
59
64
|
|
|
60
65
|
|
|
61
66
|
def _metadata_from_augmented_value(
|
|
62
|
-
|
|
67
|
+
datatype: type[SignalDatatypeT] | None,
|
|
68
|
+
value: AugmentedValue,
|
|
69
|
+
metadata: SignalMetadata,
|
|
63
70
|
) -> SignalMetadata:
|
|
64
71
|
metadata = metadata.copy()
|
|
65
|
-
if hasattr(value, "units"):
|
|
72
|
+
if hasattr(value, "units") and datatype not in (str, bool):
|
|
66
73
|
metadata["units"] = value.units
|
|
67
|
-
if
|
|
74
|
+
if (
|
|
75
|
+
hasattr(value, "precision")
|
|
76
|
+
and not isnan(value.precision)
|
|
77
|
+
and datatype is not int
|
|
78
|
+
):
|
|
68
79
|
metadata["precision"] = value.precision
|
|
69
|
-
if limits := _limits_from_augmented_value(value):
|
|
80
|
+
if (limits := _limits_from_augmented_value(value)) and datatype is not bool:
|
|
70
81
|
metadata["limits"] = limits
|
|
71
82
|
return metadata
|
|
72
83
|
|
|
@@ -100,6 +111,11 @@ class DisconnectedCaConverter(CaConverter):
|
|
|
100
111
|
raise NotImplementedError("No PV has been set as connect() has not been called")
|
|
101
112
|
|
|
102
113
|
|
|
114
|
+
class CaIntConverter(CaConverter[int]):
|
|
115
|
+
def value(self, value: AugmentedValue) -> int:
|
|
116
|
+
return int(value) # type: ignore
|
|
117
|
+
|
|
118
|
+
|
|
103
119
|
class CaArrayConverter(CaConverter[np.ndarray]):
|
|
104
120
|
def value(self, value: AugmentedValue) -> np.ndarray:
|
|
105
121
|
# A less expensive conversion
|
|
@@ -168,6 +184,9 @@ def make_converter(
|
|
|
168
184
|
Dbr, get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
|
|
169
185
|
)
|
|
170
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]
|
|
171
190
|
# Infer a datatype and converter from the dbr
|
|
172
191
|
inferred_datatype, converter_cls = _datatype_converter_from_dbr[(pv_dbr, is_array)]
|
|
173
192
|
# Some override cases
|
|
@@ -202,7 +221,7 @@ def make_converter(
|
|
|
202
221
|
and get_unique({k: v.precision for k, v in values.items()}, "precision") == 0
|
|
203
222
|
):
|
|
204
223
|
# Allow int signals to represent float records when prec is 0
|
|
205
|
-
return
|
|
224
|
+
return CaIntConverter(int, pv_dbr)
|
|
206
225
|
elif datatype in (None, inferred_datatype):
|
|
207
226
|
# If datatype matches what we are given then allow it and use inferred converter
|
|
208
227
|
return converter_cls(inferred_datatype, pv_dbr)
|
|
@@ -214,19 +233,18 @@ def make_converter(
|
|
|
214
233
|
)
|
|
215
234
|
|
|
216
235
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
236
|
+
# Cached call to avoid repeated initialization attempts
|
|
237
|
+
@cache
|
|
220
238
|
def _use_pyepics_context_if_imported():
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
ca.use_initial_context()
|
|
226
|
-
_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()
|
|
227
243
|
|
|
228
244
|
|
|
229
245
|
class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
246
|
+
"""Backend for a signal to interact with PVs over channel access."""
|
|
247
|
+
|
|
230
248
|
def __init__(
|
|
231
249
|
self,
|
|
232
250
|
datatype: type[SignalDatatypeT] | None,
|
|
@@ -247,7 +265,7 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
247
265
|
pv, format=FORMAT_CTRL, timeout=timeout
|
|
248
266
|
)
|
|
249
267
|
except CANothing as exc:
|
|
250
|
-
|
|
268
|
+
logger.debug(f"signal ca://{pv} timed out")
|
|
251
269
|
raise NotConnected(f"ca://{pv}") from exc
|
|
252
270
|
|
|
253
271
|
async def connect(self, timeout: float):
|
|
@@ -280,17 +298,33 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
280
298
|
write_value = self.initial_values[self.write_pv]
|
|
281
299
|
else:
|
|
282
300
|
write_value = self.converter.write_value(value)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
290
322
|
|
|
291
323
|
async def get_datakey(self, source: str) -> DataKey:
|
|
292
324
|
value = await self._caget(self.read_pv, FORMAT_CTRL)
|
|
293
|
-
metadata = _metadata_from_augmented_value(
|
|
325
|
+
metadata = _metadata_from_augmented_value(
|
|
326
|
+
self.datatype, value, self.converter.metadata
|
|
327
|
+
)
|
|
294
328
|
return make_datakey(
|
|
295
329
|
self.converter.datatype, self.converter.value(value), source, metadata
|
|
296
330
|
)
|
|
@@ -308,16 +342,18 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
308
342
|
return self.converter.value(value)
|
|
309
343
|
|
|
310
344
|
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
345
|
+
if callback and self.subscription:
|
|
346
|
+
msg = "Cannot set a callback when one is already set"
|
|
347
|
+
raise RuntimeError(msg)
|
|
348
|
+
|
|
349
|
+
if self.subscription:
|
|
350
|
+
self.subscription.close()
|
|
351
|
+
self.subscription = None
|
|
352
|
+
|
|
311
353
|
if callback:
|
|
312
|
-
assert (
|
|
313
|
-
not self.subscription
|
|
314
|
-
), "Cannot set a callback when one is already set"
|
|
315
354
|
self.subscription = camonitor(
|
|
316
355
|
self.read_pv,
|
|
317
356
|
lambda v: callback(self._make_reading(v)),
|
|
318
357
|
datatype=self.converter.read_dbr,
|
|
319
358
|
format=FORMAT_TIME,
|
|
320
359
|
)
|
|
321
|
-
elif self.subscription:
|
|
322
|
-
self.subscription.close()
|
|
323
|
-
self.subscription = None
|
|
@@ -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
|
|
@@ -31,6 +32,8 @@ from ophyd_async.core import (
|
|
|
31
32
|
|
|
32
33
|
from ._util import EpicsSignalBackend, format_datatype, get_supported_values
|
|
33
34
|
|
|
35
|
+
logger = logging.getLogger("ophyd_async")
|
|
36
|
+
|
|
34
37
|
|
|
35
38
|
def _limits_from_value(value: Any) -> Limits:
|
|
36
39
|
def get_limits(
|
|
@@ -39,11 +42,12 @@ def _limits_from_value(value: Any) -> Limits:
|
|
|
39
42
|
substructure = getattr(value, substucture_name, None)
|
|
40
43
|
low = getattr(substructure, low_name, nan)
|
|
41
44
|
high = getattr(substructure, high_name, nan)
|
|
42
|
-
if not (isnan(low) and isnan(high)):
|
|
45
|
+
if not (isnan(low) and isnan(high)) and not low == high == 0:
|
|
43
46
|
return LimitsRange(
|
|
44
47
|
low=None if isnan(low) else low,
|
|
45
48
|
high=None if isnan(high) else high,
|
|
46
49
|
)
|
|
50
|
+
return None
|
|
47
51
|
|
|
48
52
|
limits = Limits()
|
|
49
53
|
if limits_range := get_limits("valueAlarm", "lowAlarmLimit", "highAlarmLimit"):
|
|
@@ -60,12 +64,22 @@ def _limits_from_value(value: Any) -> Limits:
|
|
|
60
64
|
def _metadata_from_value(datatype: type[SignalDatatype], value: Any) -> SignalMetadata:
|
|
61
65
|
metadata = SignalMetadata()
|
|
62
66
|
value_data: Any = getattr(value, "value", None)
|
|
67
|
+
specifier = _get_specifier(value)
|
|
63
68
|
display_data: Any = getattr(value, "display", None)
|
|
64
|
-
if
|
|
69
|
+
if (
|
|
70
|
+
hasattr(display_data, "units")
|
|
71
|
+
and specifier[-1] in _number_specifiers
|
|
72
|
+
and datatype is not str
|
|
73
|
+
):
|
|
65
74
|
metadata["units"] = display_data.units
|
|
66
|
-
if
|
|
75
|
+
if (
|
|
76
|
+
hasattr(display_data, "precision")
|
|
77
|
+
and not isnan(display_data.precision)
|
|
78
|
+
and specifier[-1] in _float_specifiers
|
|
79
|
+
and datatype is not int
|
|
80
|
+
):
|
|
67
81
|
metadata["precision"] = display_data.precision
|
|
68
|
-
if limits := _limits_from_value(value):
|
|
82
|
+
if (limits := _limits_from_value(value)) and specifier[-1] in _number_specifiers:
|
|
69
83
|
metadata["limits"] = limits
|
|
70
84
|
# Get choices from display or value
|
|
71
85
|
if datatype is str or issubclass(datatype, StrictEnum):
|
|
@@ -84,9 +98,7 @@ class PvaConverter(Generic[SignalDatatypeT]):
|
|
|
84
98
|
self.datatype = datatype
|
|
85
99
|
|
|
86
100
|
def value(self, value: Any) -> SignalDatatypeT:
|
|
87
|
-
#
|
|
88
|
-
# invokes __pos__ operator to return an instance of
|
|
89
|
-
# the builtin base class
|
|
101
|
+
# Normally the value will be of the correct python type
|
|
90
102
|
return value["value"]
|
|
91
103
|
|
|
92
104
|
def write_value(self, value: Any) -> Any:
|
|
@@ -94,6 +106,15 @@ class PvaConverter(Generic[SignalDatatypeT]):
|
|
|
94
106
|
return value
|
|
95
107
|
|
|
96
108
|
|
|
109
|
+
class PvaIntConverter(PvaConverter[int]):
|
|
110
|
+
def __init__(self):
|
|
111
|
+
super().__init__(int)
|
|
112
|
+
|
|
113
|
+
def value(self, value: Any) -> int:
|
|
114
|
+
# Convert to an int
|
|
115
|
+
return int(value["value"])
|
|
116
|
+
|
|
117
|
+
|
|
97
118
|
class PvaLongStringConverter(PvaConverter[str]):
|
|
98
119
|
def __init__(self):
|
|
99
120
|
super().__init__(str)
|
|
@@ -174,6 +195,9 @@ class PvaTableConverter(PvaConverter[Table]):
|
|
|
174
195
|
|
|
175
196
|
|
|
176
197
|
# https://mdavidsaver.github.io/p4p/values.html
|
|
198
|
+
_float_specifiers = {"f", "d"}
|
|
199
|
+
_int_specifiers = {"b", "B", "h", "H", "i", "I", "l", "L"}
|
|
200
|
+
_number_specifiers = _float_specifiers.union(_int_specifiers)
|
|
177
201
|
_datatype_converter_from_typeid: dict[
|
|
178
202
|
tuple[str, str], tuple[type[SignalDatatype], type[PvaConverter]]
|
|
179
203
|
] = {
|
|
@@ -208,7 +232,7 @@ _datatype_converter_from_typeid: dict[
|
|
|
208
232
|
}
|
|
209
233
|
|
|
210
234
|
|
|
211
|
-
def _get_specifier(value: Value):
|
|
235
|
+
def _get_specifier(value: Value) -> str:
|
|
212
236
|
typ = value.type("value").aspy()
|
|
213
237
|
if isinstance(typ, tuple):
|
|
214
238
|
return typ[0]
|
|
@@ -223,6 +247,9 @@ def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverte
|
|
|
223
247
|
{k: _get_specifier(v) for k, v in values.items()},
|
|
224
248
|
"value type specifiers",
|
|
225
249
|
)
|
|
250
|
+
# Make the datatype canonical for the inference below
|
|
251
|
+
if datatype == typing.Sequence[str]:
|
|
252
|
+
datatype = Sequence[str]
|
|
226
253
|
# Infer a datatype and converter from the typeid and specifier
|
|
227
254
|
inferred_datatype, converter_cls = _datatype_converter_from_typeid[
|
|
228
255
|
(typeid, specifier)
|
|
@@ -258,7 +285,7 @@ def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverte
|
|
|
258
285
|
== 0
|
|
259
286
|
):
|
|
260
287
|
# Allow int signals to represent float records when prec is 0
|
|
261
|
-
return
|
|
288
|
+
return PvaIntConverter()
|
|
262
289
|
elif inferred_datatype is str and (enum_cls := get_enum_cls(datatype)):
|
|
263
290
|
# Allow strings to be used as enums until QSRV supports this
|
|
264
291
|
return PvaConverter(str)
|
|
@@ -301,18 +328,21 @@ async def pvget_with_timeout(pv: str, timeout: float) -> Any:
|
|
|
301
328
|
try:
|
|
302
329
|
return await asyncio.wait_for(context().get(pv), timeout=timeout)
|
|
303
330
|
except asyncio.TimeoutError as exc:
|
|
304
|
-
|
|
331
|
+
logger.debug(f"signal pva://{pv} timed out", exc_info=True)
|
|
305
332
|
raise NotConnected(f"pva://{pv}") from exc
|
|
306
333
|
|
|
307
334
|
|
|
308
335
|
def _pva_request_string(fields: Sequence[str]) -> str:
|
|
309
|
-
"""
|
|
310
|
-
|
|
336
|
+
"""Convert a list of requested fields into a PVA request string.
|
|
337
|
+
|
|
338
|
+
This can be passed to p4p.
|
|
311
339
|
"""
|
|
312
340
|
return f"field({','.join(fields)})"
|
|
313
341
|
|
|
314
342
|
|
|
315
343
|
class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
344
|
+
"""Backend for a signal to interact with PVs over pva."""
|
|
345
|
+
|
|
316
346
|
def __init__(
|
|
317
347
|
self,
|
|
318
348
|
datatype: type[SignalDatatypeT] | None,
|
|
@@ -383,10 +413,15 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
383
413
|
return self.converter.value(value)
|
|
384
414
|
|
|
385
415
|
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
416
|
+
if callback and self.subscription:
|
|
417
|
+
msg = "Cannot set a callback when one is already set"
|
|
418
|
+
raise RuntimeError(msg)
|
|
419
|
+
|
|
420
|
+
if self.subscription:
|
|
421
|
+
self.subscription.close()
|
|
422
|
+
self.subscription = None
|
|
423
|
+
|
|
386
424
|
if callback:
|
|
387
|
-
assert (
|
|
388
|
-
not self.subscription
|
|
389
|
-
), "Cannot set a callback when one is already set"
|
|
390
425
|
|
|
391
426
|
async def async_callback(v):
|
|
392
427
|
callback(self._make_reading(v))
|
|
@@ -397,6 +432,3 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
397
432
|
self.subscription = context().monitor(
|
|
398
433
|
self.read_pv, async_callback, request=request
|
|
399
434
|
)
|
|
400
|
-
elif self.subscription:
|
|
401
|
-
self.subscription.close()
|
|
402
|
-
self.subscription = 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)
|