ophyd-async 0.2.0__py3-none-any.whl → 0.3a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/__init__.py +1 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +16 -10
- ophyd_async/core/_providers.py +38 -5
- ophyd_async/core/async_status.py +3 -3
- ophyd_async/core/detector.py +211 -62
- ophyd_async/core/device.py +45 -38
- ophyd_async/core/device_save_loader.py +96 -23
- ophyd_async/core/flyer.py +30 -244
- ophyd_async/core/signal.py +47 -21
- ophyd_async/core/signal_backend.py +7 -4
- ophyd_async/core/sim_signal_backend.py +30 -18
- ophyd_async/core/standard_readable.py +4 -2
- ophyd_async/core/utils.py +93 -30
- ophyd_async/epics/_backend/_aioca.py +30 -36
- ophyd_async/epics/_backend/_p4p.py +75 -41
- ophyd_async/epics/_backend/common.py +25 -0
- ophyd_async/epics/areadetector/__init__.py +4 -0
- ophyd_async/epics/areadetector/aravis.py +69 -0
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +73 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +37 -25
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +154 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +4 -4
- ophyd_async/epics/areadetector/pilatus.py +50 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +21 -7
- ophyd_async/epics/areadetector/writers/hdf_writer.py +26 -15
- ophyd_async/epics/demo/__init__.py +33 -3
- ophyd_async/epics/motion/motor.py +20 -14
- ophyd_async/epics/pvi/__init__.py +3 -0
- ophyd_async/epics/pvi/pvi.py +318 -0
- ophyd_async/epics/signal/__init__.py +0 -2
- ophyd_async/epics/signal/signal.py +26 -9
- ophyd_async/panda/__init__.py +19 -5
- ophyd_async/panda/_common_blocks.py +49 -0
- ophyd_async/panda/_hdf_panda.py +48 -0
- ophyd_async/panda/_panda_controller.py +37 -0
- ophyd_async/panda/_trigger.py +39 -0
- ophyd_async/panda/_utils.py +15 -0
- ophyd_async/panda/writers/__init__.py +3 -0
- ophyd_async/panda/writers/_hdf_writer.py +220 -0
- ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
- ophyd_async/planstubs/__init__.py +5 -0
- ophyd_async/planstubs/prepare_trigger_and_dets.py +57 -0
- ophyd_async/protocols.py +73 -0
- ophyd_async/sim/__init__.py +11 -0
- ophyd_async/sim/demo/__init__.py +3 -0
- ophyd_async/sim/demo/sim_motor.py +116 -0
- ophyd_async/sim/pattern_generator.py +318 -0
- ophyd_async/sim/sim_pattern_detector_control.py +55 -0
- ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
- ophyd_async/sim/sim_pattern_generator.py +37 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/METADATA +20 -76
- ophyd_async-0.3a2.dist-info/RECORD +76 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd_async/panda/panda.py +0 -294
- ophyd_async-0.2.0.dist-info/RECORD +0 -53
- /ophyd_async/panda/{table.py → _table.py} +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/top_level.txt +0 -0
|
@@ -2,17 +2,17 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import inspect
|
|
5
|
-
import re
|
|
6
5
|
import time
|
|
7
6
|
from collections import abc
|
|
8
7
|
from dataclasses import dataclass
|
|
9
8
|
from enum import Enum
|
|
10
9
|
from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin
|
|
11
10
|
|
|
11
|
+
import numpy as np
|
|
12
12
|
from bluesky.protocols import Descriptor, Dtype, Reading
|
|
13
13
|
|
|
14
14
|
from .signal_backend import SignalBackend
|
|
15
|
-
from .utils import ReadingValueCallback, T, get_dtype
|
|
15
|
+
from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype
|
|
16
16
|
|
|
17
17
|
primitive_dtypes: Dict[type, Dtype] = {
|
|
18
18
|
str: "string",
|
|
@@ -37,11 +37,16 @@ class SimConverter(Generic[T]):
|
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
40
|
+
dtype = type(value)
|
|
41
|
+
if np.issubdtype(dtype, np.integer):
|
|
42
|
+
dtype = int
|
|
43
|
+
elif np.issubdtype(dtype, np.floating):
|
|
44
|
+
dtype = float
|
|
40
45
|
assert (
|
|
41
|
-
|
|
46
|
+
dtype in primitive_dtypes
|
|
42
47
|
), f"invalid converter for value of type {type(value)}"
|
|
43
|
-
|
|
44
|
-
return
|
|
48
|
+
dtype_name = primitive_dtypes[dtype]
|
|
49
|
+
return {"source": source, "dtype": dtype_name, "shape": []}
|
|
45
50
|
|
|
46
51
|
def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
|
|
47
52
|
if datatype is None:
|
|
@@ -52,7 +57,7 @@ class SimConverter(Generic[T]):
|
|
|
52
57
|
|
|
53
58
|
class SimArrayConverter(SimConverter):
|
|
54
59
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
55
|
-
return
|
|
60
|
+
return {"source": source, "dtype": "array", "shape": [len(value)]}
|
|
56
61
|
|
|
57
62
|
def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
|
|
58
63
|
if datatype is None:
|
|
@@ -76,9 +81,7 @@ class SimEnumConverter(SimConverter):
|
|
|
76
81
|
|
|
77
82
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
78
83
|
choices = [e.value for e in self.enum_class]
|
|
79
|
-
return
|
|
80
|
-
source=source, dtype="string", shape=[], choices=choices
|
|
81
|
-
) # type: ignore
|
|
84
|
+
return {"source": source, "dtype": "string", "shape": [], "choices": choices} # type: ignore
|
|
82
85
|
|
|
83
86
|
def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
|
|
84
87
|
if datatype is None:
|
|
@@ -109,23 +112,32 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
109
112
|
"""An simulated backend to a Signal, created with ``Signal.connect(sim=True)``"""
|
|
110
113
|
|
|
111
114
|
_value: T
|
|
112
|
-
_initial_value: T
|
|
115
|
+
_initial_value: Optional[T]
|
|
113
116
|
_timestamp: float
|
|
114
117
|
_severity: int
|
|
115
118
|
|
|
116
|
-
def __init__(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
datatype: Optional[Type[T]],
|
|
122
|
+
initial_value: Optional[T] = None,
|
|
123
|
+
) -> None:
|
|
119
124
|
self.datatype = datatype
|
|
120
|
-
self.pv = source
|
|
121
125
|
self.converter: SimConverter = DisconnectedSimConverter()
|
|
126
|
+
self._initial_value = initial_value
|
|
122
127
|
self.put_proceeds = asyncio.Event()
|
|
123
128
|
self.put_proceeds.set()
|
|
124
129
|
self.callback: Optional[ReadingValueCallback[T]] = None
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
def source(self, name: str) -> str:
|
|
132
|
+
return f"soft://{name}"
|
|
133
|
+
|
|
134
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
127
135
|
self.converter = make_converter(self.datatype)
|
|
128
|
-
self._initial_value
|
|
136
|
+
if self._initial_value is None:
|
|
137
|
+
self._initial_value = self.converter.make_initial_value(self.datatype)
|
|
138
|
+
else:
|
|
139
|
+
# convert potentially unconverted initial value passed to init method
|
|
140
|
+
self._initial_value = self.converter.write_value(self._initial_value)
|
|
129
141
|
self._severity = 0
|
|
130
142
|
|
|
131
143
|
await self.put(None)
|
|
@@ -152,8 +164,8 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
152
164
|
if self.callback:
|
|
153
165
|
self.callback(reading, self._value)
|
|
154
166
|
|
|
155
|
-
async def get_descriptor(self) -> Descriptor:
|
|
156
|
-
return self.converter.descriptor(
|
|
167
|
+
async def get_descriptor(self, source: str) -> Descriptor:
|
|
168
|
+
return self.converter.descriptor(source, self._value)
|
|
157
169
|
|
|
158
170
|
async def get_reading(self) -> Reading:
|
|
159
171
|
return self.converter.reading(self._value, self._timestamp, self._severity)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from typing import Dict, Sequence, Tuple
|
|
2
2
|
|
|
3
|
-
from bluesky.protocols import
|
|
3
|
+
from bluesky.protocols import Descriptor, Reading, Stageable
|
|
4
|
+
|
|
5
|
+
from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
|
|
4
6
|
|
|
5
7
|
from .async_status import AsyncStatus
|
|
6
8
|
from .device import Device
|
|
@@ -8,7 +10,7 @@ from .signal import SignalR
|
|
|
8
10
|
from .utils import merge_gathered_dicts
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class StandardReadable(Device,
|
|
13
|
+
class StandardReadable(Device, AsyncReadable, AsyncConfigurable, Stageable):
|
|
12
14
|
"""Device that owns its children and provides useful default behavior.
|
|
13
15
|
|
|
14
16
|
- When its name is set it renames child Devices
|
ophyd_async/core/utils.py
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
|
-
|
|
4
|
+
import logging
|
|
5
|
+
from typing import (
|
|
6
|
+
Awaitable,
|
|
7
|
+
Callable,
|
|
8
|
+
Dict,
|
|
9
|
+
Iterable,
|
|
10
|
+
List,
|
|
11
|
+
Optional,
|
|
12
|
+
Type,
|
|
13
|
+
TypeVar,
|
|
14
|
+
Union,
|
|
15
|
+
)
|
|
3
16
|
|
|
4
17
|
import numpy as np
|
|
5
18
|
from bluesky.protocols import Reading
|
|
@@ -11,46 +24,79 @@ Callback = Callable[[T], None]
|
|
|
11
24
|
#: monitor updates
|
|
12
25
|
ReadingValueCallback = Callable[[Reading, T], None]
|
|
13
26
|
DEFAULT_TIMEOUT = 10.0
|
|
27
|
+
ErrorText = Union[str, Dict[str, Exception]]
|
|
14
28
|
|
|
15
29
|
|
|
16
30
|
class NotConnected(Exception):
|
|
17
31
|
"""Exception to be raised if a `Device.connect` is cancelled"""
|
|
18
32
|
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
_indent_width = " "
|
|
34
|
+
|
|
35
|
+
def __init__(self, errors: ErrorText):
|
|
36
|
+
"""
|
|
37
|
+
NotConnected holds a mapping of device/signal names to
|
|
38
|
+
errors.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
errors: ErrorText
|
|
43
|
+
Mapping of device name to Exception or another NotConnected.
|
|
44
|
+
Alternatively a string with the signal error text.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
self._errors = errors
|
|
48
|
+
|
|
49
|
+
def _format_sub_errors(self, name: str, error: Exception, indent="") -> str:
|
|
50
|
+
if isinstance(error, NotConnected):
|
|
51
|
+
error_txt = ":" + error.format_error_string(indent + self._indent_width)
|
|
52
|
+
elif isinstance(error, Exception):
|
|
53
|
+
error_txt = ": " + err_str + "\n" if (err_str := str(error)) else "\n"
|
|
54
|
+
else:
|
|
55
|
+
raise RuntimeError(
|
|
56
|
+
f"Unexpected type `{type(error)}`, expected an Exception"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
string = f"{indent}{name}: {type(error).__name__}" + error_txt
|
|
60
|
+
return string
|
|
61
|
+
|
|
62
|
+
def format_error_string(self, indent="") -> str:
|
|
63
|
+
if not isinstance(self._errors, dict) and not isinstance(self._errors, str):
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
f"Unexpected type `{type(self._errors)}` " "expected `str` or `dict`"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if isinstance(self._errors, str):
|
|
69
|
+
return " " + self._errors + "\n"
|
|
70
|
+
|
|
71
|
+
string = "\n"
|
|
72
|
+
for name, error in self._errors.items():
|
|
73
|
+
string += self._format_sub_errors(name, error, indent=indent)
|
|
74
|
+
return string
|
|
21
75
|
|
|
22
76
|
def __str__(self) -> str:
|
|
23
|
-
return "
|
|
77
|
+
return self.format_error_string(indent="")
|
|
24
78
|
|
|
25
79
|
|
|
26
80
|
async def wait_for_connection(**coros: Awaitable[None]):
|
|
27
|
-
"""Call many underlying signals, accumulating
|
|
81
|
+
"""Call many underlying signals, accumulating exceptions and returning them
|
|
28
82
|
|
|
29
|
-
|
|
30
|
-
------
|
|
31
|
-
`NotConnected` if cancelled
|
|
83
|
+
Expected kwargs should be a mapping of names to coroutine tasks to execute.
|
|
32
84
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
lines += [f" {line}" for line in e.lines]
|
|
49
|
-
raise NotConnected(*lines)
|
|
50
|
-
else:
|
|
51
|
-
# Wait for everything to foreground the exceptions
|
|
52
|
-
for f in list(done) + list(pending):
|
|
53
|
-
await f
|
|
85
|
+
results = await asyncio.gather(*coros.values(), return_exceptions=True)
|
|
86
|
+
exceptions = {}
|
|
87
|
+
|
|
88
|
+
for name, result in zip(coros, results):
|
|
89
|
+
if isinstance(result, Exception):
|
|
90
|
+
exceptions[name] = result
|
|
91
|
+
if not isinstance(result, NotConnected):
|
|
92
|
+
logging.exception(
|
|
93
|
+
f"device `{name}` raised unexpected exception "
|
|
94
|
+
f"{type(result).__name__}",
|
|
95
|
+
exc_info=result,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if exceptions:
|
|
99
|
+
raise NotConnected(exceptions)
|
|
54
100
|
|
|
55
101
|
|
|
56
102
|
def get_dtype(typ: Type) -> Optional[np.dtype]:
|
|
@@ -86,7 +132,7 @@ def get_unique(values: Dict[str, T], types: str) -> T:
|
|
|
86
132
|
|
|
87
133
|
|
|
88
134
|
async def merge_gathered_dicts(
|
|
89
|
-
coros: Iterable[Awaitable[Dict[str, T]]]
|
|
135
|
+
coros: Iterable[Awaitable[Dict[str, T]]],
|
|
90
136
|
) -> Dict[str, T]:
|
|
91
137
|
"""Merge dictionaries produced by a sequence of coroutines.
|
|
92
138
|
|
|
@@ -102,3 +148,20 @@ async def merge_gathered_dicts(
|
|
|
102
148
|
|
|
103
149
|
async def gather_list(coros: Iterable[Awaitable[T]]) -> List[T]:
|
|
104
150
|
return await asyncio.gather(*coros)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def in_micros(t: float) -> int:
|
|
154
|
+
"""
|
|
155
|
+
Converts between a positive number of seconds and an equivalent
|
|
156
|
+
number of microseconds.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
t (float): A time in seconds
|
|
160
|
+
Raises:
|
|
161
|
+
ValueError: if t < 0
|
|
162
|
+
Returns:
|
|
163
|
+
t (int): A time in microseconds, rounded up to the nearest whole microsecond,
|
|
164
|
+
"""
|
|
165
|
+
if t < 0:
|
|
166
|
+
raise ValueError(f"Expected a positive time in seconds, got {t!r}")
|
|
167
|
+
return int(np.ceil(t * 1e6))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import sys
|
|
2
|
-
from asyncio import CancelledError
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from typing import Any, Dict, Optional, Sequence, Type, Union
|
|
@@ -8,6 +8,7 @@ from aioca import (
|
|
|
8
8
|
FORMAT_CTRL,
|
|
9
9
|
FORMAT_RAW,
|
|
10
10
|
FORMAT_TIME,
|
|
11
|
+
CANothing,
|
|
11
12
|
Subscription,
|
|
12
13
|
caget,
|
|
13
14
|
camonitor,
|
|
@@ -18,7 +19,6 @@ from bluesky.protocols import Descriptor, Dtype, Reading
|
|
|
18
19
|
from epicscorelibs.ca import dbr
|
|
19
20
|
|
|
20
21
|
from ophyd_async.core import (
|
|
21
|
-
NotConnected,
|
|
22
22
|
ReadingValueCallback,
|
|
23
23
|
SignalBackend,
|
|
24
24
|
T,
|
|
@@ -26,6 +26,9 @@ from ophyd_async.core import (
|
|
|
26
26
|
get_unique,
|
|
27
27
|
wait_for_connection,
|
|
28
28
|
)
|
|
29
|
+
from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected
|
|
30
|
+
|
|
31
|
+
from .common import get_supported_enum_class
|
|
29
32
|
|
|
30
33
|
dbr_to_dtype: Dict[Dbr, Dtype] = {
|
|
31
34
|
dbr.DBR_STRING: "string",
|
|
@@ -49,14 +52,14 @@ class CaConverter:
|
|
|
49
52
|
return value
|
|
50
53
|
|
|
51
54
|
def reading(self, value: AugmentedValue):
|
|
52
|
-
return
|
|
53
|
-
value
|
|
54
|
-
timestamp
|
|
55
|
-
alarm_severity
|
|
56
|
-
|
|
55
|
+
return {
|
|
56
|
+
"value": self.value(value),
|
|
57
|
+
"timestamp": value.timestamp,
|
|
58
|
+
"alarm_severity": -1 if value.severity > 2 else value.severity,
|
|
59
|
+
}
|
|
57
60
|
|
|
58
61
|
def descriptor(self, source: str, value: AugmentedValue) -> Descriptor:
|
|
59
|
-
return
|
|
62
|
+
return {"source": source, "dtype": dbr_to_dtype[value.datatype], "shape": []}
|
|
60
63
|
|
|
61
64
|
|
|
62
65
|
class CaLongStrConverter(CaConverter):
|
|
@@ -71,7 +74,7 @@ class CaLongStrConverter(CaConverter):
|
|
|
71
74
|
|
|
72
75
|
class CaArrayConverter(CaConverter):
|
|
73
76
|
def descriptor(self, source: str, value: AugmentedValue) -> Descriptor:
|
|
74
|
-
return
|
|
77
|
+
return {"source": source, "dtype": "array", "shape": [len(value)]}
|
|
75
78
|
|
|
76
79
|
|
|
77
80
|
@dataclass
|
|
@@ -89,9 +92,7 @@ class CaEnumConverter(CaConverter):
|
|
|
89
92
|
|
|
90
93
|
def descriptor(self, source: str, value: AugmentedValue) -> Descriptor:
|
|
91
94
|
choices = [e.value for e in self.enum_class]
|
|
92
|
-
return
|
|
93
|
-
source=source, dtype="string", shape=[], choices=choices
|
|
94
|
-
) # type: ignore
|
|
95
|
+
return {"source": source, "dtype": "string", "shape": [], "choices": choices}
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
class DisconnectedCaConverter(CaConverter):
|
|
@@ -137,19 +138,7 @@ def make_converter(
|
|
|
137
138
|
pv_choices = get_unique(
|
|
138
139
|
{k: tuple(v.enums) for k, v in values.items()}, "choices"
|
|
139
140
|
)
|
|
140
|
-
|
|
141
|
-
if not issubclass(datatype, Enum):
|
|
142
|
-
raise TypeError(f"{pv} has type Enum not {datatype.__name__}")
|
|
143
|
-
if not issubclass(datatype, str):
|
|
144
|
-
raise TypeError(f"{pv} has type Enum but doesn't inherit from String")
|
|
145
|
-
choices = tuple(v.value for v in datatype)
|
|
146
|
-
if set(choices) != set(pv_choices):
|
|
147
|
-
raise TypeError(f"{pv} has choices {pv_choices} not {choices}")
|
|
148
|
-
enum_class = datatype
|
|
149
|
-
else:
|
|
150
|
-
enum_class = Enum( # type: ignore
|
|
151
|
-
"GeneratedChoices", {x: x for x in pv_choices}, type=str
|
|
152
|
-
)
|
|
141
|
+
enum_class = get_supported_enum_class(pv, datatype, pv_choices)
|
|
153
142
|
return CaEnumConverter(dbr.DBR_STRING, None, enum_class)
|
|
154
143
|
else:
|
|
155
144
|
value = list(values.values())[0]
|
|
@@ -181,26 +170,31 @@ class CaSignalBackend(SignalBackend[T]):
|
|
|
181
170
|
self.write_pv = write_pv
|
|
182
171
|
self.initial_values: Dict[str, AugmentedValue] = {}
|
|
183
172
|
self.converter: CaConverter = DisconnectedCaConverter(None, None)
|
|
184
|
-
self.source = f"ca://{self.read_pv}"
|
|
185
173
|
self.subscription: Optional[Subscription] = None
|
|
186
174
|
|
|
187
|
-
|
|
175
|
+
def source(self, name: str):
|
|
176
|
+
return f"ca://{self.read_pv}"
|
|
177
|
+
|
|
178
|
+
async def _store_initial_value(self, pv, timeout: float = DEFAULT_TIMEOUT):
|
|
188
179
|
try:
|
|
189
|
-
self.initial_values[pv] = await caget(
|
|
190
|
-
|
|
191
|
-
|
|
180
|
+
self.initial_values[pv] = await caget(
|
|
181
|
+
pv, format=FORMAT_CTRL, timeout=timeout
|
|
182
|
+
)
|
|
183
|
+
except CANothing as exc:
|
|
184
|
+
logging.debug(f"signal ca://{pv} timed out")
|
|
185
|
+
raise NotConnected(f"ca://{pv}") from exc
|
|
192
186
|
|
|
193
|
-
async def connect(self):
|
|
187
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT):
|
|
194
188
|
_use_pyepics_context_if_imported()
|
|
195
189
|
if self.read_pv != self.write_pv:
|
|
196
190
|
# Different, need to connect both
|
|
197
191
|
await wait_for_connection(
|
|
198
|
-
read_pv=self._store_initial_value(self.read_pv),
|
|
199
|
-
write_pv=self._store_initial_value(self.write_pv),
|
|
192
|
+
read_pv=self._store_initial_value(self.read_pv, timeout=timeout),
|
|
193
|
+
write_pv=self._store_initial_value(self.write_pv, timeout=timeout),
|
|
200
194
|
)
|
|
201
195
|
else:
|
|
202
196
|
# The same, so only need to connect one
|
|
203
|
-
await self._store_initial_value(self.read_pv)
|
|
197
|
+
await self._store_initial_value(self.read_pv, timeout=timeout)
|
|
204
198
|
self.converter = make_converter(self.datatype, self.initial_values)
|
|
205
199
|
|
|
206
200
|
async def put(self, value: Optional[T], wait=True, timeout=None):
|
|
@@ -224,9 +218,9 @@ class CaSignalBackend(SignalBackend[T]):
|
|
|
224
218
|
timeout=None,
|
|
225
219
|
)
|
|
226
220
|
|
|
227
|
-
async def get_descriptor(self) -> Descriptor:
|
|
221
|
+
async def get_descriptor(self, source: str) -> Descriptor:
|
|
228
222
|
value = await self._caget(FORMAT_CTRL)
|
|
229
|
-
return self.converter.descriptor(
|
|
223
|
+
return self.converter.descriptor(source, value)
|
|
230
224
|
|
|
231
225
|
async def get_reading(self) -> Reading:
|
|
232
226
|
value = await self._caget(FORMAT_TIME)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import atexit
|
|
3
|
-
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from enum import Enum
|
|
6
7
|
from typing import Any, Dict, List, Optional, Sequence, Type, Union
|
|
@@ -10,7 +11,6 @@ from p4p import Value
|
|
|
10
11
|
from p4p.client.asyncio import Context, Subscription
|
|
11
12
|
|
|
12
13
|
from ophyd_async.core import (
|
|
13
|
-
NotConnected,
|
|
14
14
|
ReadingValueCallback,
|
|
15
15
|
SignalBackend,
|
|
16
16
|
T,
|
|
@@ -18,6 +18,9 @@ from ophyd_async.core import (
|
|
|
18
18
|
get_unique,
|
|
19
19
|
wait_for_connection,
|
|
20
20
|
)
|
|
21
|
+
from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected
|
|
22
|
+
|
|
23
|
+
from .common import get_supported_enum_class
|
|
21
24
|
|
|
22
25
|
# https://mdavidsaver.github.io/p4p/values.html
|
|
23
26
|
specifier_to_dtype: Dict[str, Dtype] = {
|
|
@@ -46,15 +49,15 @@ class PvaConverter:
|
|
|
46
49
|
def reading(self, value):
|
|
47
50
|
ts = value["timeStamp"]
|
|
48
51
|
sv = value["alarm"]["severity"]
|
|
49
|
-
return
|
|
50
|
-
value
|
|
51
|
-
timestamp
|
|
52
|
-
alarm_severity
|
|
53
|
-
|
|
52
|
+
return {
|
|
53
|
+
"value": self.value(value),
|
|
54
|
+
"timestamp": ts["secondsPastEpoch"] + ts["nanoseconds"] * 1e-9,
|
|
55
|
+
"alarm_severity": -1 if sv > 2 else sv,
|
|
56
|
+
}
|
|
54
57
|
|
|
55
58
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
56
59
|
dtype = specifier_to_dtype[value.type().aspy("value")]
|
|
57
|
-
return
|
|
60
|
+
return {"source": source, "dtype": dtype, "shape": []}
|
|
58
61
|
|
|
59
62
|
def metadata_fields(self) -> List[str]:
|
|
60
63
|
"""
|
|
@@ -71,7 +74,7 @@ class PvaConverter:
|
|
|
71
74
|
|
|
72
75
|
class PvaArrayConverter(PvaConverter):
|
|
73
76
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
74
|
-
return
|
|
77
|
+
return {"source": source, "dtype": "array", "shape": [len(value["value"])]}
|
|
75
78
|
|
|
76
79
|
|
|
77
80
|
class PvaNDArrayConverter(PvaConverter):
|
|
@@ -95,7 +98,7 @@ class PvaNDArrayConverter(PvaConverter):
|
|
|
95
98
|
|
|
96
99
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
97
100
|
dims = self._get_dimensions(value)
|
|
98
|
-
return
|
|
101
|
+
return {"source": source, "dtype": "array", "shape": dims}
|
|
99
102
|
|
|
100
103
|
def write_value(self, value):
|
|
101
104
|
# No clear use-case for writing directly to an NDArray, and some
|
|
@@ -119,9 +122,7 @@ class PvaEnumConverter(PvaConverter):
|
|
|
119
122
|
|
|
120
123
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
121
124
|
choices = [e.value for e in self.enum_class]
|
|
122
|
-
return
|
|
123
|
-
source=source, dtype="string", shape=[], choices=choices
|
|
124
|
-
) # type: ignore
|
|
125
|
+
return {"source": source, "dtype": "string", "shape": [], "choices": choices}
|
|
125
126
|
|
|
126
127
|
|
|
127
128
|
class PvaEnumBoolConverter(PvaConverter):
|
|
@@ -129,7 +130,7 @@ class PvaEnumBoolConverter(PvaConverter):
|
|
|
129
130
|
return value["value"]["index"]
|
|
130
131
|
|
|
131
132
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
132
|
-
return
|
|
133
|
+
return {"source": source, "dtype": "integer", "shape": []}
|
|
133
134
|
|
|
134
135
|
|
|
135
136
|
class PvaTableConverter(PvaConverter):
|
|
@@ -138,7 +139,33 @@ class PvaTableConverter(PvaConverter):
|
|
|
138
139
|
|
|
139
140
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
140
141
|
# This is wrong, but defer until we know how to actually describe a table
|
|
141
|
-
return
|
|
142
|
+
return {"source": source, "dtype": "object", "shape": []} # type: ignore
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class PvaDictConverter(PvaConverter):
|
|
146
|
+
def reading(self, value):
|
|
147
|
+
ts = time.time()
|
|
148
|
+
value = value.todict()
|
|
149
|
+
# Alarm severity is vacuously 0 for a table
|
|
150
|
+
return {"value": value, "timestamp": ts, "alarm_severity": 0}
|
|
151
|
+
|
|
152
|
+
def value(self, value: Value):
|
|
153
|
+
return value.todict()
|
|
154
|
+
|
|
155
|
+
def descriptor(self, source: str, value) -> Descriptor:
|
|
156
|
+
raise NotImplementedError("Describing Dict signals not currently supported")
|
|
157
|
+
|
|
158
|
+
def metadata_fields(self) -> List[str]:
|
|
159
|
+
"""
|
|
160
|
+
Fields to request from PVA for metadata.
|
|
161
|
+
"""
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
def value_fields(self) -> List[str]:
|
|
165
|
+
"""
|
|
166
|
+
Fields to request from PVA for the value.
|
|
167
|
+
"""
|
|
168
|
+
return []
|
|
142
169
|
|
|
143
170
|
|
|
144
171
|
class DisconnectedPvaConverter(PvaConverter):
|
|
@@ -149,7 +176,9 @@ class DisconnectedPvaConverter(PvaConverter):
|
|
|
149
176
|
def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConverter:
|
|
150
177
|
pv = list(values)[0]
|
|
151
178
|
typeid = get_unique({k: v.getID() for k, v in values.items()}, "typeids")
|
|
152
|
-
typ = get_unique(
|
|
179
|
+
typ = get_unique(
|
|
180
|
+
{k: type(v.get("value")) for k, v in values.items()}, "value types"
|
|
181
|
+
)
|
|
153
182
|
if "NTScalarArray" in typeid and typ == list:
|
|
154
183
|
# Waveform of strings, check we wanted this
|
|
155
184
|
if datatype and datatype != Sequence[str]:
|
|
@@ -185,24 +214,15 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
|
|
|
185
214
|
pv_choices = get_unique(
|
|
186
215
|
{k: tuple(v["value"]["choices"]) for k, v in values.items()}, "choices"
|
|
187
216
|
)
|
|
188
|
-
|
|
189
|
-
if not issubclass(datatype, Enum):
|
|
190
|
-
raise TypeError(f"{pv} has type Enum not {datatype.__name__}")
|
|
191
|
-
choices = tuple(v.value for v in datatype)
|
|
192
|
-
if set(choices) != set(pv_choices):
|
|
193
|
-
raise TypeError(f"{pv} has choices {pv_choices} not {choices}")
|
|
194
|
-
enum_class = datatype
|
|
195
|
-
else:
|
|
196
|
-
enum_class = Enum( # type: ignore
|
|
197
|
-
"GeneratedChoices", {x or "_": x for x in pv_choices}, type=str
|
|
198
|
-
)
|
|
199
|
-
return PvaEnumConverter(enum_class)
|
|
217
|
+
return PvaEnumConverter(get_supported_enum_class(pv, datatype, pv_choices))
|
|
200
218
|
elif "NTScalar" in typeid:
|
|
201
219
|
if datatype and not issubclass(typ, datatype):
|
|
202
220
|
raise TypeError(f"{pv} has type {typ.__name__} not {datatype.__name__}")
|
|
203
221
|
return PvaConverter()
|
|
204
222
|
elif "NTTable" in typeid:
|
|
205
223
|
return PvaTableConverter()
|
|
224
|
+
elif "structure" in typeid:
|
|
225
|
+
return PvaDictConverter()
|
|
206
226
|
else:
|
|
207
227
|
raise TypeError(f"{pv}: Unsupported typeid {typeid}")
|
|
208
228
|
|
|
@@ -216,9 +236,12 @@ class PvaSignalBackend(SignalBackend[T]):
|
|
|
216
236
|
self.write_pv = write_pv
|
|
217
237
|
self.initial_values: Dict[str, Any] = {}
|
|
218
238
|
self.converter: PvaConverter = DisconnectedPvaConverter()
|
|
219
|
-
self.source = f"pva://{self.read_pv}"
|
|
220
239
|
self.subscription: Optional[Subscription] = None
|
|
221
240
|
|
|
241
|
+
@property
|
|
242
|
+
def source(self, name: str):
|
|
243
|
+
return f"pva://{self.read_pv}"
|
|
244
|
+
|
|
222
245
|
@property
|
|
223
246
|
def ctxt(self) -> Context:
|
|
224
247
|
if PvaSignalBackend._ctxt is None:
|
|
@@ -233,22 +256,25 @@ class PvaSignalBackend(SignalBackend[T]):
|
|
|
233
256
|
|
|
234
257
|
return PvaSignalBackend._ctxt
|
|
235
258
|
|
|
236
|
-
async def _store_initial_value(self, pv):
|
|
259
|
+
async def _store_initial_value(self, pv, timeout: float = DEFAULT_TIMEOUT):
|
|
237
260
|
try:
|
|
238
|
-
self.initial_values[pv] = await
|
|
239
|
-
|
|
240
|
-
|
|
261
|
+
self.initial_values[pv] = await asyncio.wait_for(
|
|
262
|
+
self.ctxt.get(pv), timeout=timeout
|
|
263
|
+
)
|
|
264
|
+
except asyncio.TimeoutError as exc:
|
|
265
|
+
logging.debug(f"signal pva://{pv} timed out", exc_info=True)
|
|
266
|
+
raise NotConnected(f"pva://{pv}") from exc
|
|
241
267
|
|
|
242
|
-
async def connect(self):
|
|
268
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT):
|
|
243
269
|
if self.read_pv != self.write_pv:
|
|
244
270
|
# Different, need to connect both
|
|
245
271
|
await wait_for_connection(
|
|
246
|
-
read_pv=self._store_initial_value(self.read_pv),
|
|
247
|
-
write_pv=self._store_initial_value(self.write_pv),
|
|
272
|
+
read_pv=self._store_initial_value(self.read_pv, timeout=timeout),
|
|
273
|
+
write_pv=self._store_initial_value(self.write_pv, timeout=timeout),
|
|
248
274
|
)
|
|
249
275
|
else:
|
|
250
276
|
# The same, so only need to connect one
|
|
251
|
-
await self._store_initial_value(self.read_pv)
|
|
277
|
+
await self._store_initial_value(self.read_pv, timeout=timeout)
|
|
252
278
|
self.converter = make_converter(self.datatype, self.initial_values)
|
|
253
279
|
|
|
254
280
|
async def put(self, value: Optional[T], wait=True, timeout=None):
|
|
@@ -256,12 +282,20 @@ class PvaSignalBackend(SignalBackend[T]):
|
|
|
256
282
|
write_value = self.initial_values[self.write_pv]
|
|
257
283
|
else:
|
|
258
284
|
write_value = self.converter.write_value(value)
|
|
259
|
-
coro = self.ctxt.put(self.write_pv,
|
|
260
|
-
|
|
285
|
+
coro = self.ctxt.put(self.write_pv, {"value": write_value}, wait=wait)
|
|
286
|
+
try:
|
|
287
|
+
await asyncio.wait_for(coro, timeout)
|
|
288
|
+
except asyncio.TimeoutError as exc:
|
|
289
|
+
logging.debug(
|
|
290
|
+
f"signal pva://{self.write_pv} timed out \
|
|
291
|
+
put value: {write_value}",
|
|
292
|
+
exc_info=True,
|
|
293
|
+
)
|
|
294
|
+
raise NotConnected(f"pva://{self.write_pv}") from exc
|
|
261
295
|
|
|
262
|
-
async def get_descriptor(self) -> Descriptor:
|
|
296
|
+
async def get_descriptor(self, source: str) -> Descriptor:
|
|
263
297
|
value = await self.ctxt.get(self.read_pv)
|
|
264
|
-
return self.converter.descriptor(
|
|
298
|
+
return self.converter.descriptor(source, value)
|
|
265
299
|
|
|
266
300
|
def _pva_request_string(self, fields: List[str]) -> str:
|
|
267
301
|
"""
|