ophyd-async 0.7.0__py3-none-any.whl → 0.8.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/_version.py +2 -2
- ophyd_async/core/__init__.py +23 -8
- ophyd_async/core/_detector.py +5 -10
- ophyd_async/core/_device.py +139 -66
- ophyd_async/core/_device_filler.py +191 -0
- ophyd_async/core/_device_save_loader.py +6 -7
- ophyd_async/core/_mock_signal_backend.py +32 -40
- ophyd_async/core/_mock_signal_utils.py +22 -16
- ophyd_async/core/_protocol.py +28 -8
- ophyd_async/core/_readable.py +5 -5
- ophyd_async/core/_signal.py +140 -152
- ophyd_async/core/_signal_backend.py +131 -64
- ophyd_async/core/_soft_signal_backend.py +125 -194
- ophyd_async/core/_status.py +22 -6
- ophyd_async/core/_table.py +97 -100
- ophyd_async/core/_utils.py +71 -18
- ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
- ophyd_async/epics/adaravis/_aravis_io.py +7 -5
- ophyd_async/epics/adcore/_core_io.py +4 -6
- ophyd_async/epics/adcore/_hdf_writer.py +2 -2
- ophyd_async/epics/adcore/_utils.py +15 -10
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
- ophyd_async/epics/adkinetix/_kinetix_io.py +3 -4
- ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus_io.py +2 -3
- ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
- ophyd_async/epics/advimba/__init__.py +4 -1
- ophyd_async/epics/advimba/_vimba_controller.py +6 -3
- ophyd_async/epics/advimba/_vimba_io.py +7 -8
- ophyd_async/epics/demo/_sensor.py +8 -4
- ophyd_async/epics/eiger/_eiger.py +1 -2
- ophyd_async/epics/eiger/_eiger_controller.py +1 -1
- ophyd_async/epics/eiger/_eiger_io.py +2 -4
- ophyd_async/epics/eiger/_odin_io.py +4 -4
- ophyd_async/epics/pvi/__init__.py +2 -2
- ophyd_async/epics/pvi/_pvi.py +56 -321
- ophyd_async/epics/signal/__init__.py +3 -4
- ophyd_async/epics/signal/_aioca.py +184 -236
- ophyd_async/epics/signal/_common.py +35 -49
- ophyd_async/epics/signal/_p4p.py +254 -387
- ophyd_async/epics/signal/_signal.py +63 -21
- ophyd_async/fastcs/core.py +9 -0
- ophyd_async/fastcs/panda/__init__.py +4 -4
- ophyd_async/fastcs/panda/_block.py +18 -13
- ophyd_async/fastcs/panda/_control.py +3 -5
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
- ophyd_async/fastcs/panda/_table.py +29 -51
- ophyd_async/fastcs/panda/_trigger.py +8 -8
- ophyd_async/fastcs/panda/_writer.py +2 -5
- ophyd_async/plan_stubs/_ensure_connected.py +3 -1
- ophyd_async/plan_stubs/_fly.py +2 -2
- ophyd_async/plan_stubs/_nd_attributes.py +5 -4
- ophyd_async/py.typed +0 -0
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
- ophyd_async/tango/__init__.py +2 -4
- ophyd_async/tango/base_devices/_base_device.py +76 -143
- ophyd_async/tango/demo/_counter.py +2 -2
- ophyd_async/tango/demo/_mover.py +2 -2
- ophyd_async/tango/signal/__init__.py +2 -4
- ophyd_async/tango/signal/_signal.py +29 -50
- ophyd_async/tango/signal/_tango_transport.py +38 -40
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/METADATA +8 -12
- ophyd_async-0.8.0a2.dist-info/RECORD +110 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/signal/_epics_transport.py +0 -34
- ophyd_async-0.7.0.dist-info/RECORD +0 -108
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import inspect
|
|
2
1
|
import logging
|
|
3
2
|
import sys
|
|
4
3
|
from collections.abc import Sequence
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from enum import Enum
|
|
7
4
|
from math import isnan, nan
|
|
8
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, Generic, cast
|
|
9
6
|
|
|
10
7
|
import numpy as np
|
|
11
8
|
from aioca import (
|
|
@@ -21,233 +18,201 @@ from aioca import (
|
|
|
21
18
|
from aioca.types import AugmentedValue, Dbr, Format
|
|
22
19
|
from bluesky.protocols import Reading
|
|
23
20
|
from epicscorelibs.ca import dbr
|
|
24
|
-
from event_model import DataKey
|
|
25
|
-
from event_model.documents.event_descriptor import Dtype
|
|
21
|
+
from event_model import DataKey, Limits, LimitsRange
|
|
26
22
|
|
|
27
23
|
from ophyd_async.core import (
|
|
28
|
-
|
|
24
|
+
Array1D,
|
|
25
|
+
Callback,
|
|
29
26
|
NotConnected,
|
|
30
|
-
ReadingValueCallback,
|
|
31
|
-
RuntimeSubsetEnum,
|
|
32
27
|
SignalBackend,
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
SignalDatatype,
|
|
29
|
+
SignalDatatypeT,
|
|
30
|
+
SignalMetadata,
|
|
31
|
+
get_enum_cls,
|
|
35
32
|
get_unique,
|
|
33
|
+
make_datakey,
|
|
36
34
|
wait_for_connection,
|
|
37
35
|
)
|
|
38
36
|
|
|
39
|
-
from ._common import
|
|
40
|
-
|
|
41
|
-
dbr_to_dtype: dict[Dbr, Dtype] = {
|
|
42
|
-
dbr.DBR_STRING: "string",
|
|
43
|
-
dbr.DBR_SHORT: "integer",
|
|
44
|
-
dbr.DBR_FLOAT: "number",
|
|
45
|
-
dbr.DBR_CHAR: "string",
|
|
46
|
-
dbr.DBR_LONG: "integer",
|
|
47
|
-
dbr.DBR_DOUBLE: "number",
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _data_key_from_augmented_value(
|
|
52
|
-
value: AugmentedValue,
|
|
53
|
-
*,
|
|
54
|
-
choices: list[str] | None = None,
|
|
55
|
-
dtype: Dtype | None = None,
|
|
56
|
-
) -> DataKey:
|
|
57
|
-
"""Use the return value of get with FORMAT_CTRL to construct a DataKey
|
|
58
|
-
describing the signal. See docstring of AugmentedValue for expected
|
|
59
|
-
value fields by DBR type.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
value (AugmentedValue): Description of the the return type of a DB record
|
|
63
|
-
choices: Optional list of enum choices to pass as metadata in the datakey
|
|
64
|
-
dtype: Optional override dtype when AugmentedValue is ambiguous, e.g. booleans
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
DataKey: A rich DataKey describing the DB record
|
|
68
|
-
"""
|
|
69
|
-
source = f"ca://{value.name}"
|
|
70
|
-
assert value.ok, f"Error reading {source}: {value}"
|
|
71
|
-
|
|
72
|
-
scalar = value.element_count == 1
|
|
73
|
-
dtype = dtype or dbr_to_dtype[value.datatype] # type: ignore
|
|
74
|
-
|
|
75
|
-
dtype_numpy = np.dtype(dbr.DbrCodeToType[value.datatype].dtype).descr[0][1]
|
|
76
|
-
|
|
77
|
-
d = DataKey(
|
|
78
|
-
source=source,
|
|
79
|
-
dtype=dtype if scalar else "array",
|
|
80
|
-
# Ignore until https://github.com/bluesky/event-model/issues/308
|
|
81
|
-
dtype_numpy=dtype_numpy, # type: ignore
|
|
82
|
-
# strictly value.element_count >= len(value)
|
|
83
|
-
shape=[] if scalar else [len(value)],
|
|
84
|
-
)
|
|
85
|
-
for key in common_meta:
|
|
86
|
-
attr = getattr(value, key, nan)
|
|
87
|
-
if isinstance(attr, str) or not isnan(attr):
|
|
88
|
-
d[key] = attr
|
|
89
|
-
|
|
90
|
-
if choices is not None:
|
|
91
|
-
d["choices"] = choices # type: ignore
|
|
92
|
-
|
|
93
|
-
if limits := _limits_from_augmented_value(value):
|
|
94
|
-
d["limits"] = limits # type: ignore
|
|
95
|
-
|
|
96
|
-
return d
|
|
37
|
+
from ._common import format_datatype, get_supported_values
|
|
97
38
|
|
|
98
39
|
|
|
99
40
|
def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
100
|
-
def get_limits(limit: str) ->
|
|
41
|
+
def get_limits(limit: str) -> LimitsRange | None:
|
|
101
42
|
low = getattr(value, f"lower_{limit}_limit", nan)
|
|
102
43
|
high = getattr(value, f"upper_{limit}_limit", nan)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
alarm=get_limits("alarm"),
|
|
109
|
-
control=get_limits("ctrl"),
|
|
110
|
-
display=get_limits("disp"),
|
|
111
|
-
warning=get_limits("warning"),
|
|
112
|
-
)
|
|
113
|
-
|
|
44
|
+
if not (isnan(low) and isnan(high)):
|
|
45
|
+
return LimitsRange(
|
|
46
|
+
low=None if isnan(low) else low,
|
|
47
|
+
high=None if isnan(high) else high,
|
|
48
|
+
)
|
|
114
49
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
50
|
+
limits = Limits()
|
|
51
|
+
if limits_range := get_limits("alarm"):
|
|
52
|
+
limits["alarm"] = limits_range
|
|
53
|
+
if limits_range := get_limits("ctrl"):
|
|
54
|
+
limits["control"] = limits_range
|
|
55
|
+
if limits_range := get_limits("disp"):
|
|
56
|
+
limits["display"] = limits_range
|
|
57
|
+
if limits_range := get_limits("warning"):
|
|
58
|
+
limits["warning"] = limits_range
|
|
59
|
+
return limits
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _metadata_from_augmented_value(
|
|
63
|
+
value: AugmentedValue, metadata: SignalMetadata
|
|
64
|
+
) -> SignalMetadata:
|
|
65
|
+
metadata = metadata.copy()
|
|
66
|
+
if hasattr(value, "units"):
|
|
67
|
+
metadata["units"] = value.units
|
|
68
|
+
if hasattr(value, "precision") and not isnan(value.precision):
|
|
69
|
+
metadata["precision"] = value.precision
|
|
70
|
+
if limits := _limits_from_augmented_value(value):
|
|
71
|
+
metadata["limits"] = limits
|
|
72
|
+
return metadata
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CaConverter(Generic[SignalDatatypeT]):
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
datatype: type[SignalDatatypeT],
|
|
79
|
+
read_dbr: Dbr,
|
|
80
|
+
write_dbr: Dbr | None = None,
|
|
81
|
+
metadata: SignalMetadata | None = None,
|
|
82
|
+
):
|
|
83
|
+
self.datatype = datatype
|
|
84
|
+
self.read_dbr: Dbr = read_dbr
|
|
85
|
+
self.write_dbr: Dbr | None = write_dbr
|
|
86
|
+
self.metadata = metadata or SignalMetadata()
|
|
119
87
|
|
|
120
|
-
def write_value(self, value) -> Any:
|
|
88
|
+
def write_value(self, value: Any) -> Any:
|
|
89
|
+
# The ca library will do the conversion for us
|
|
121
90
|
return value
|
|
122
91
|
|
|
123
|
-
def value(self, value: AugmentedValue):
|
|
92
|
+
def value(self, value: AugmentedValue) -> SignalDatatypeT:
|
|
124
93
|
# for channel access ca_xxx classes, this
|
|
125
94
|
# invokes __pos__ operator to return an instance of
|
|
126
95
|
# the builtin base class
|
|
127
96
|
return +value # type: ignore
|
|
128
97
|
|
|
129
|
-
def reading(self, value: AugmentedValue) -> Reading:
|
|
130
|
-
return {
|
|
131
|
-
"value": self.value(value),
|
|
132
|
-
"timestamp": value.timestamp,
|
|
133
|
-
"alarm_severity": -1 if value.severity > 2 else value.severity,
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
def get_datakey(self, value: AugmentedValue) -> DataKey:
|
|
137
|
-
return _data_key_from_augmented_value(value)
|
|
138
98
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return super().__init__(dbr.DBR_CHAR_STR, dbr.DBR_CHAR_STR)
|
|
143
|
-
|
|
144
|
-
def write_value(self, value: str):
|
|
145
|
-
# Add a null in here as this is what the commandline caput does
|
|
146
|
-
# TODO: this should be in the server so check if it can be pushed to asyn
|
|
147
|
-
return value + "\0"
|
|
99
|
+
class DisconnectedCaConverter(CaConverter):
|
|
100
|
+
def __getattribute__(self, __name: str) -> Any:
|
|
101
|
+
raise NotImplementedError("No PV has been set as connect() has not been called")
|
|
148
102
|
|
|
149
103
|
|
|
150
|
-
class CaArrayConverter(CaConverter):
|
|
151
|
-
def value(self, value: AugmentedValue):
|
|
104
|
+
class CaArrayConverter(CaConverter[np.ndarray]):
|
|
105
|
+
def value(self, value: AugmentedValue) -> np.ndarray:
|
|
106
|
+
# A less expensive conversion
|
|
152
107
|
return np.array(value, copy=False)
|
|
153
108
|
|
|
154
109
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
values or orders, we put treat an Enum signal as a string, and cache the
|
|
159
|
-
choices on this class.
|
|
160
|
-
"""
|
|
110
|
+
class CaSequenceStrConverter(CaConverter[Sequence[str]]):
|
|
111
|
+
def value(self, value: AugmentedValue) -> Sequence[str]:
|
|
112
|
+
return [str(v) for v in value] # type: ignore
|
|
161
113
|
|
|
162
|
-
choices: dict[str, str]
|
|
163
114
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
else:
|
|
168
|
-
return value
|
|
115
|
+
class CaLongStrConverter(CaConverter[str]):
|
|
116
|
+
def __init__(self):
|
|
117
|
+
super().__init__(str, dbr.DBR_CHAR_STR, dbr.DBR_CHAR_STR)
|
|
169
118
|
|
|
170
|
-
def
|
|
171
|
-
|
|
119
|
+
def write_value_and_dbr(self, value: Any) -> Any:
|
|
120
|
+
# Add a null in here as this is what the commandline caput does
|
|
121
|
+
# TODO: this should be in the server so check if it can be pushed to asyn
|
|
122
|
+
return value + "\0"
|
|
172
123
|
|
|
173
|
-
def get_datakey(self, value: AugmentedValue) -> DataKey:
|
|
174
|
-
# Sometimes DBR_TYPE returns as String, must pass choices still
|
|
175
|
-
return _data_key_from_augmented_value(value, choices=list(self.choices.keys()))
|
|
176
124
|
|
|
125
|
+
class CaBoolConverter(CaConverter[bool]):
|
|
126
|
+
def __init__(self):
|
|
127
|
+
super().__init__(bool, dbr.DBR_SHORT)
|
|
177
128
|
|
|
178
|
-
@dataclass
|
|
179
|
-
class CaBoolConverter(CaConverter):
|
|
180
129
|
def value(self, value: AugmentedValue) -> bool:
|
|
181
130
|
return bool(value)
|
|
182
131
|
|
|
183
|
-
def get_datakey(self, value: AugmentedValue) -> DataKey:
|
|
184
|
-
return _data_key_from_augmented_value(value, dtype="boolean")
|
|
185
132
|
|
|
133
|
+
class CaEnumConverter(CaConverter[str]):
|
|
134
|
+
def __init__(self, supported_values: dict[str, str]):
|
|
135
|
+
self.supported_values = supported_values
|
|
136
|
+
super().__init__(
|
|
137
|
+
str, dbr.DBR_STRING, metadata=SignalMetadata(choices=list(supported_values))
|
|
138
|
+
)
|
|
186
139
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
140
|
+
def value(self, value: AugmentedValue) -> str:
|
|
141
|
+
return self.supported_values[str(value)]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
_datatype_converter_from_dbr: dict[
|
|
145
|
+
tuple[Dbr, bool], tuple[type[SignalDatatype], type[CaConverter]]
|
|
146
|
+
] = {
|
|
147
|
+
(dbr.DBR_STRING, False): (str, CaConverter),
|
|
148
|
+
(dbr.DBR_SHORT, False): (int, CaConverter),
|
|
149
|
+
(dbr.DBR_FLOAT, False): (float, CaConverter),
|
|
150
|
+
(dbr.DBR_ENUM, False): (str, CaConverter),
|
|
151
|
+
(dbr.DBR_CHAR, False): (int, CaConverter),
|
|
152
|
+
(dbr.DBR_LONG, False): (int, CaConverter),
|
|
153
|
+
(dbr.DBR_DOUBLE, False): (float, CaConverter),
|
|
154
|
+
(dbr.DBR_STRING, True): (Sequence[str], CaSequenceStrConverter),
|
|
155
|
+
(dbr.DBR_SHORT, True): (Array1D[np.int16], CaArrayConverter),
|
|
156
|
+
(dbr.DBR_FLOAT, True): (Array1D[np.float32], CaArrayConverter),
|
|
157
|
+
(dbr.DBR_ENUM, True): (Sequence[str], CaSequenceStrConverter),
|
|
158
|
+
(dbr.DBR_CHAR, True): (Array1D[np.uint8], CaArrayConverter),
|
|
159
|
+
(dbr.DBR_LONG, True): (Array1D[np.int32], CaArrayConverter),
|
|
160
|
+
(dbr.DBR_DOUBLE, True): (Array1D[np.float64], CaArrayConverter),
|
|
161
|
+
}
|
|
190
162
|
|
|
191
163
|
|
|
192
164
|
def make_converter(
|
|
193
165
|
datatype: type | None, values: dict[str, AugmentedValue]
|
|
194
166
|
) -> CaConverter:
|
|
195
167
|
pv = list(values)[0]
|
|
196
|
-
pv_dbr =
|
|
168
|
+
pv_dbr = cast(
|
|
169
|
+
Dbr, get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
|
|
170
|
+
)
|
|
197
171
|
is_array = bool([v for v in values.values() if v.element_count > 1])
|
|
198
|
-
|
|
172
|
+
# Infer a datatype and converter from the dbr
|
|
173
|
+
inferred_datatype, converter_cls = _datatype_converter_from_dbr[(pv_dbr, is_array)]
|
|
174
|
+
# Some override cases
|
|
175
|
+
if is_array and pv_dbr == dbr.DBR_CHAR and datatype is str:
|
|
199
176
|
# Override waveform of chars to be treated as string
|
|
200
177
|
return CaLongStrConverter()
|
|
201
|
-
elif is_array and pv_dbr == dbr.
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
datatype_dtype = get_dtype(datatype)
|
|
205
|
-
if not datatype_dtype or not np.can_cast(datatype_dtype, np.str_):
|
|
206
|
-
raise TypeError(f"{pv} has type [str] not {datatype.__name__}")
|
|
207
|
-
return CaArrayConverter(pv_dbr, None)
|
|
208
|
-
elif is_array:
|
|
209
|
-
pv_dtype = get_unique({k: v.dtype for k, v in values.items()}, "dtypes") # type: ignore
|
|
210
|
-
# This is an array
|
|
211
|
-
if datatype:
|
|
212
|
-
# Check we wanted an array of this type
|
|
213
|
-
dtype = get_dtype(datatype)
|
|
214
|
-
if not dtype:
|
|
215
|
-
raise TypeError(f"{pv} has type [{pv_dtype}] not {datatype.__name__}")
|
|
216
|
-
if dtype != pv_dtype:
|
|
217
|
-
raise TypeError(f"{pv} has type [{pv_dtype}] not [{dtype}]")
|
|
218
|
-
return CaArrayConverter(pv_dbr, None) # type: ignore
|
|
219
|
-
elif pv_dbr == dbr.DBR_ENUM and datatype is bool:
|
|
220
|
-
# Database can't do bools, so are often representated as enums,
|
|
221
|
-
# CA can do int
|
|
222
|
-
pv_choices_len = get_unique(
|
|
178
|
+
elif not is_array and datatype is bool and pv_dbr == dbr.DBR_ENUM:
|
|
179
|
+
# Database can't do bools, so are often representated as enums of len 2
|
|
180
|
+
pv_num_choices = get_unique(
|
|
223
181
|
{k: len(v.enums) for k, v in values.items()}, "number of choices"
|
|
224
182
|
)
|
|
225
|
-
if
|
|
226
|
-
raise TypeError(f"{pv} has {
|
|
227
|
-
return CaBoolConverter(
|
|
228
|
-
elif pv_dbr == dbr.DBR_ENUM:
|
|
229
|
-
# This is an Enum
|
|
183
|
+
if pv_num_choices != 2:
|
|
184
|
+
raise TypeError(f"{pv} has {pv_num_choices} choices, can't map to bool")
|
|
185
|
+
return CaBoolConverter()
|
|
186
|
+
elif not is_array and pv_dbr == dbr.DBR_ENUM:
|
|
230
187
|
pv_choices = get_unique(
|
|
231
188
|
{k: tuple(v.enums) for k, v in values.items()}, "choices"
|
|
232
189
|
)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
and get_unique({k: v.precision for k, v in values.items()}, "precision")
|
|
243
|
-
== 0
|
|
190
|
+
if enum_cls := get_enum_cls(datatype):
|
|
191
|
+
# If explicitly requested then check
|
|
192
|
+
return CaEnumConverter(get_supported_values(pv, enum_cls, pv_choices))
|
|
193
|
+
elif datatype in (None, str):
|
|
194
|
+
# Drop to string for safety, but retain choices as metadata
|
|
195
|
+
return CaConverter(
|
|
196
|
+
str,
|
|
197
|
+
dbr.DBR_STRING,
|
|
198
|
+
metadata=SignalMetadata(choices=list(pv_choices)),
|
|
244
199
|
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
200
|
+
elif (
|
|
201
|
+
inferred_datatype is float
|
|
202
|
+
and datatype is int
|
|
203
|
+
and get_unique({k: v.precision for k, v in values.items()}, "precision") == 0
|
|
204
|
+
):
|
|
205
|
+
# Allow int signals to represent float records when prec is 0
|
|
206
|
+
return CaConverter(int, pv_dbr)
|
|
207
|
+
elif datatype in (None, inferred_datatype):
|
|
208
|
+
# If datatype matches what we are given then allow it and use inferred converter
|
|
209
|
+
return converter_cls(inferred_datatype, pv_dbr)
|
|
210
|
+
if pv_dbr == dbr.DBR_ENUM:
|
|
211
|
+
inferred_datatype = "str | SubsetEnum | StrictEnum"
|
|
212
|
+
raise TypeError(
|
|
213
|
+
f"{pv} with inferred datatype {format_datatype(inferred_datatype)}"
|
|
214
|
+
f" cannot be coerced to {format_datatype(datatype)}"
|
|
215
|
+
)
|
|
251
216
|
|
|
252
217
|
|
|
253
218
|
_tried_pyepics = False
|
|
@@ -262,42 +227,24 @@ def _use_pyepics_context_if_imported():
|
|
|
262
227
|
_tried_pyepics = True
|
|
263
228
|
|
|
264
229
|
|
|
265
|
-
class CaSignalBackend(SignalBackend[
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
str,
|
|
271
|
-
|
|
272
|
-
Enum,
|
|
273
|
-
RuntimeSubsetEnum,
|
|
274
|
-
np.ndarray,
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
@classmethod
|
|
278
|
-
def datatype_allowed(cls, dtype: Any) -> bool:
|
|
279
|
-
stripped_origin = get_origin(dtype) or dtype
|
|
280
|
-
if dtype is None:
|
|
281
|
-
return True
|
|
282
|
-
|
|
283
|
-
return inspect.isclass(stripped_origin) and issubclass(
|
|
284
|
-
stripped_origin, cls._ALLOWED_DATATYPES
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
def __init__(self, datatype: type[T] | None, read_pv: str, write_pv: str):
|
|
288
|
-
self.datatype = datatype
|
|
289
|
-
if not CaSignalBackend.datatype_allowed(self.datatype):
|
|
290
|
-
raise TypeError(f"Given datatype {self.datatype} unsupported in CA.")
|
|
230
|
+
class CaSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
231
|
+
def __init__(
|
|
232
|
+
self,
|
|
233
|
+
datatype: type[SignalDatatypeT] | None,
|
|
234
|
+
read_pv: str = "",
|
|
235
|
+
write_pv: str = "",
|
|
236
|
+
):
|
|
291
237
|
self.read_pv = read_pv
|
|
292
238
|
self.write_pv = write_pv
|
|
239
|
+
self.converter: CaConverter = DisconnectedCaConverter(float, dbr.DBR_DOUBLE)
|
|
293
240
|
self.initial_values: dict[str, AugmentedValue] = {}
|
|
294
|
-
self.converter: CaConverter = DisconnectedCaConverter(None, None)
|
|
295
241
|
self.subscription: Subscription | None = None
|
|
242
|
+
super().__init__(datatype)
|
|
296
243
|
|
|
297
|
-
def source(self, name: str):
|
|
298
|
-
return f"ca://{self.read_pv}"
|
|
244
|
+
def source(self, name: str, read: bool):
|
|
245
|
+
return f"ca://{self.read_pv if read else self.write_pv}"
|
|
299
246
|
|
|
300
|
-
async def _store_initial_value(self, pv, timeout: float
|
|
247
|
+
async def _store_initial_value(self, pv: str, timeout: float):
|
|
301
248
|
try:
|
|
302
249
|
self.initial_values[pv] = await caget(
|
|
303
250
|
pv, format=FORMAT_CTRL, timeout=timeout
|
|
@@ -306,7 +253,7 @@ class CaSignalBackend(SignalBackend[T]):
|
|
|
306
253
|
logging.debug(f"signal ca://{pv} timed out")
|
|
307
254
|
raise NotConnected(f"ca://{pv}") from exc
|
|
308
255
|
|
|
309
|
-
async def connect(self, timeout: float
|
|
256
|
+
async def connect(self, timeout: float):
|
|
310
257
|
_use_pyepics_context_if_imported()
|
|
311
258
|
if self.read_pv != self.write_pv:
|
|
312
259
|
# Different, need to connect both
|
|
@@ -319,7 +266,19 @@ class CaSignalBackend(SignalBackend[T]):
|
|
|
319
266
|
await self._store_initial_value(self.read_pv, timeout=timeout)
|
|
320
267
|
self.converter = make_converter(self.datatype, self.initial_values)
|
|
321
268
|
|
|
322
|
-
async def
|
|
269
|
+
async def _caget(self, pv: str, format: Format) -> AugmentedValue:
|
|
270
|
+
return await caget(
|
|
271
|
+
pv, datatype=self.converter.read_dbr, format=format, timeout=None
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def _make_reading(self, value: AugmentedValue) -> Reading[SignalDatatypeT]:
|
|
275
|
+
return {
|
|
276
|
+
"value": self.converter.value(value),
|
|
277
|
+
"timestamp": value.timestamp,
|
|
278
|
+
"alarm_severity": -1 if value.severity > 2 else value.severity,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async def put(self, value: SignalDatatypeT | None, wait: bool):
|
|
323
282
|
if value is None:
|
|
324
283
|
write_value = self.initial_values[self.write_pv]
|
|
325
284
|
else:
|
|
@@ -329,50 +288,39 @@ class CaSignalBackend(SignalBackend[T]):
|
|
|
329
288
|
write_value,
|
|
330
289
|
datatype=self.converter.write_dbr,
|
|
331
290
|
wait=wait,
|
|
332
|
-
timeout=timeout,
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
async def _caget(self, format: Format) -> AugmentedValue:
|
|
336
|
-
return await caget(
|
|
337
|
-
self.read_pv,
|
|
338
|
-
datatype=self.converter.read_dbr,
|
|
339
|
-
format=format,
|
|
340
291
|
timeout=None,
|
|
341
292
|
)
|
|
342
293
|
|
|
343
294
|
async def get_datakey(self, source: str) -> DataKey:
|
|
344
|
-
value = await self._caget(FORMAT_CTRL)
|
|
345
|
-
|
|
295
|
+
value = await self._caget(self.read_pv, FORMAT_CTRL)
|
|
296
|
+
metadata = _metadata_from_augmented_value(value, self.converter.metadata)
|
|
297
|
+
return make_datakey(
|
|
298
|
+
self.converter.datatype, self.converter.value(value), source, metadata
|
|
299
|
+
)
|
|
346
300
|
|
|
347
|
-
async def get_reading(self) -> Reading:
|
|
348
|
-
value = await self._caget(FORMAT_TIME)
|
|
349
|
-
return self.
|
|
301
|
+
async def get_reading(self) -> Reading[SignalDatatypeT]:
|
|
302
|
+
value = await self._caget(self.read_pv, FORMAT_TIME)
|
|
303
|
+
return self._make_reading(value)
|
|
350
304
|
|
|
351
|
-
async def get_value(self) ->
|
|
352
|
-
value = await self._caget(FORMAT_RAW)
|
|
305
|
+
async def get_value(self) -> SignalDatatypeT:
|
|
306
|
+
value = await self._caget(self.read_pv, FORMAT_RAW)
|
|
353
307
|
return self.converter.value(value)
|
|
354
308
|
|
|
355
|
-
async def get_setpoint(self) ->
|
|
356
|
-
value = await
|
|
357
|
-
self.write_pv,
|
|
358
|
-
datatype=self.converter.read_dbr,
|
|
359
|
-
format=FORMAT_RAW,
|
|
360
|
-
timeout=None,
|
|
361
|
-
)
|
|
309
|
+
async def get_setpoint(self) -> SignalDatatypeT:
|
|
310
|
+
value = await self._caget(self.write_pv, FORMAT_RAW)
|
|
362
311
|
return self.converter.value(value)
|
|
363
312
|
|
|
364
|
-
def set_callback(self, callback:
|
|
313
|
+
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
365
314
|
if callback:
|
|
366
315
|
assert (
|
|
367
316
|
not self.subscription
|
|
368
317
|
), "Cannot set a callback when one is already set"
|
|
369
318
|
self.subscription = camonitor(
|
|
370
319
|
self.read_pv,
|
|
371
|
-
lambda v: callback(self.
|
|
320
|
+
lambda v: callback(self._make_reading(v)),
|
|
372
321
|
datatype=self.converter.read_dbr,
|
|
373
322
|
format=FORMAT_TIME,
|
|
374
323
|
)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
self.subscription.close()
|
|
324
|
+
elif self.subscription:
|
|
325
|
+
self.subscription.close()
|
|
378
326
|
self.subscription = None
|
|
@@ -1,57 +1,43 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Any, get_args, get_origin
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import numpy as np
|
|
5
5
|
|
|
6
|
-
from ophyd_async.core import
|
|
7
|
-
|
|
8
|
-
common_meta = {
|
|
9
|
-
"units",
|
|
10
|
-
"precision",
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class LimitPair(TypedDict):
|
|
15
|
-
high: float | None
|
|
16
|
-
low: float | None
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class Limits(TypedDict):
|
|
20
|
-
alarm: LimitPair
|
|
21
|
-
control: LimitPair
|
|
22
|
-
display: LimitPair
|
|
23
|
-
warning: LimitPair
|
|
6
|
+
from ophyd_async.core import SubsetEnum, get_dtype, get_enum_cls
|
|
24
7
|
|
|
25
8
|
|
|
26
9
|
def get_supported_values(
|
|
27
10
|
pv: str,
|
|
28
|
-
datatype: type
|
|
29
|
-
pv_choices:
|
|
11
|
+
datatype: type,
|
|
12
|
+
pv_choices: Sequence[str],
|
|
30
13
|
) -> dict[str, str]:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
raise TypeError(
|
|
41
|
-
f"{pv} is type Enum but {datatype} does not inherit from String."
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
choices = tuple(v.value for v in datatype)
|
|
14
|
+
enum_cls = get_enum_cls(datatype)
|
|
15
|
+
if not enum_cls:
|
|
16
|
+
raise TypeError(f"{datatype} is not an Enum")
|
|
17
|
+
choices = [v.value for v in enum_cls]
|
|
18
|
+
error_msg = f"{pv} has choices {pv_choices}, but {datatype} requested {choices} "
|
|
19
|
+
if issubclass(enum_cls, SubsetEnum):
|
|
20
|
+
if not set(choices).issubset(pv_choices):
|
|
21
|
+
raise TypeError(error_msg + "to be a subset of them.")
|
|
22
|
+
else:
|
|
45
23
|
if set(choices) != set(pv_choices):
|
|
46
|
-
raise TypeError(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
)
|
|
24
|
+
raise TypeError(error_msg + "to be strictly equal to them.")
|
|
25
|
+
|
|
26
|
+
# Take order from the pv choices
|
|
27
|
+
supported_values = {x: x for x in pv_choices}
|
|
28
|
+
# But override those that we specify via the datatype
|
|
29
|
+
for v in enum_cls:
|
|
30
|
+
supported_values[v.value] = v
|
|
31
|
+
return supported_values
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def format_datatype(datatype: Any) -> str:
|
|
35
|
+
if get_origin(datatype) is np.ndarray and get_args(datatype)[0] == tuple[int]:
|
|
36
|
+
dtype = get_dtype(datatype)
|
|
37
|
+
return f"Array1D[np.{dtype.name}]"
|
|
38
|
+
elif get_origin(datatype) is Sequence:
|
|
39
|
+
return f"Sequence[{get_args(datatype)[0].__name__}]"
|
|
40
|
+
elif isinstance(datatype, type):
|
|
41
|
+
return datatype.__name__
|
|
42
|
+
else:
|
|
43
|
+
return str(datatype)
|