ophyd-async 0.5.2__py3-none-any.whl → 0.7.0__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 +10 -1
- ophyd_async/__main__.py +12 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +15 -7
- ophyd_async/core/_detector.py +133 -87
- ophyd_async/core/_device.py +19 -16
- ophyd_async/core/_device_save_loader.py +30 -19
- ophyd_async/core/_flyer.py +6 -19
- ophyd_async/core/_hdf_dataset.py +8 -9
- ophyd_async/core/_log.py +3 -1
- ophyd_async/core/_mock_signal_backend.py +11 -9
- ophyd_async/core/_mock_signal_utils.py +8 -5
- ophyd_async/core/_protocol.py +7 -7
- ophyd_async/core/_providers.py +11 -11
- ophyd_async/core/_readable.py +30 -22
- ophyd_async/core/_signal.py +52 -51
- ophyd_async/core/_signal_backend.py +20 -7
- ophyd_async/core/_soft_signal_backend.py +62 -32
- ophyd_async/core/_status.py +7 -9
- ophyd_async/core/_table.py +146 -0
- ophyd_async/core/_utils.py +24 -28
- ophyd_async/epics/adaravis/_aravis_controller.py +20 -19
- ophyd_async/epics/adaravis/_aravis_io.py +2 -1
- ophyd_async/epics/adcore/_core_io.py +2 -0
- ophyd_async/epics/adcore/_core_logic.py +4 -5
- ophyd_async/epics/adcore/_hdf_writer.py +19 -8
- ophyd_async/epics/adcore/_single_trigger.py +1 -1
- ophyd_async/epics/adcore/_utils.py +5 -6
- ophyd_async/epics/adkinetix/_kinetix_controller.py +20 -15
- ophyd_async/epics/adpilatus/_pilatus_controller.py +22 -18
- ophyd_async/epics/adsimdetector/_sim.py +7 -6
- ophyd_async/epics/adsimdetector/_sim_controller.py +22 -17
- ophyd_async/epics/advimba/_vimba_controller.py +22 -17
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/sensor.db +0 -1
- ophyd_async/epics/eiger/_eiger.py +1 -1
- ophyd_async/epics/eiger/_eiger_controller.py +18 -18
- ophyd_async/epics/eiger/_odin_io.py +6 -5
- ophyd_async/epics/motor.py +8 -10
- ophyd_async/epics/pvi/_pvi.py +30 -33
- ophyd_async/epics/signal/_aioca.py +55 -25
- ophyd_async/epics/signal/_common.py +3 -10
- ophyd_async/epics/signal/_epics_transport.py +11 -8
- ophyd_async/epics/signal/_p4p.py +79 -30
- ophyd_async/epics/signal/_signal.py +6 -8
- ophyd_async/fastcs/panda/__init__.py +0 -6
- ophyd_async/fastcs/panda/_block.py +7 -0
- ophyd_async/fastcs/panda/_control.py +16 -17
- ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
- ophyd_async/fastcs/panda/_table.py +77 -138
- ophyd_async/fastcs/panda/_trigger.py +4 -5
- ophyd_async/fastcs/panda/_utils.py +3 -2
- ophyd_async/fastcs/panda/_writer.py +30 -15
- ophyd_async/plan_stubs/_fly.py +15 -17
- ophyd_async/plan_stubs/_nd_attributes.py +12 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +27 -21
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
- ophyd_async/sim/demo/_sim_motor.py +2 -1
- ophyd_async/tango/__init__.py +45 -0
- ophyd_async/tango/base_devices/__init__.py +4 -0
- ophyd_async/tango/base_devices/_base_device.py +225 -0
- ophyd_async/tango/base_devices/_tango_readable.py +33 -0
- ophyd_async/tango/demo/__init__.py +12 -0
- ophyd_async/tango/demo/_counter.py +37 -0
- ophyd_async/tango/demo/_detector.py +42 -0
- ophyd_async/tango/demo/_mover.py +77 -0
- ophyd_async/tango/demo/_tango/__init__.py +3 -0
- ophyd_async/tango/demo/_tango/_servers.py +108 -0
- ophyd_async/tango/signal/__init__.py +39 -0
- ophyd_async/tango/signal/_signal.py +223 -0
- ophyd_async/tango/signal/_tango_transport.py +764 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/METADATA +50 -45
- ophyd_async-0.7.0.dist-info/RECORD +108 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/WHEEL +1 -1
- ophyd_async-0.5.2.dist-info/RECORD +0 -95
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import logging
|
|
2
3
|
import sys
|
|
4
|
+
from collections.abc import Sequence
|
|
3
5
|
from dataclasses import dataclass
|
|
4
6
|
from enum import Enum
|
|
5
7
|
from math import isnan, nan
|
|
6
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, get_origin
|
|
7
9
|
|
|
8
10
|
import numpy as np
|
|
9
11
|
from aioca import (
|
|
@@ -17,13 +19,16 @@ from aioca import (
|
|
|
17
19
|
caput,
|
|
18
20
|
)
|
|
19
21
|
from aioca.types import AugmentedValue, Dbr, Format
|
|
20
|
-
from bluesky.protocols import
|
|
22
|
+
from bluesky.protocols import Reading
|
|
21
23
|
from epicscorelibs.ca import dbr
|
|
24
|
+
from event_model import DataKey
|
|
25
|
+
from event_model.documents.event_descriptor import Dtype
|
|
22
26
|
|
|
23
27
|
from ophyd_async.core import (
|
|
24
28
|
DEFAULT_TIMEOUT,
|
|
25
29
|
NotConnected,
|
|
26
30
|
ReadingValueCallback,
|
|
31
|
+
RuntimeSubsetEnum,
|
|
27
32
|
SignalBackend,
|
|
28
33
|
T,
|
|
29
34
|
get_dtype,
|
|
@@ -33,7 +38,7 @@ from ophyd_async.core import (
|
|
|
33
38
|
|
|
34
39
|
from ._common import LimitPair, Limits, common_meta, get_supported_values
|
|
35
40
|
|
|
36
|
-
dbr_to_dtype:
|
|
41
|
+
dbr_to_dtype: dict[Dbr, Dtype] = {
|
|
37
42
|
dbr.DBR_STRING: "string",
|
|
38
43
|
dbr.DBR_SHORT: "integer",
|
|
39
44
|
dbr.DBR_FLOAT: "number",
|
|
@@ -46,8 +51,8 @@ dbr_to_dtype: Dict[Dbr, Dtype] = {
|
|
|
46
51
|
def _data_key_from_augmented_value(
|
|
47
52
|
value: AugmentedValue,
|
|
48
53
|
*,
|
|
49
|
-
choices:
|
|
50
|
-
dtype:
|
|
54
|
+
choices: list[str] | None = None,
|
|
55
|
+
dtype: Dtype | None = None,
|
|
51
56
|
) -> DataKey:
|
|
52
57
|
"""Use the return value of get with FORMAT_CTRL to construct a DataKey
|
|
53
58
|
describing the signal. See docstring of AugmentedValue for expected
|
|
@@ -65,14 +70,15 @@ def _data_key_from_augmented_value(
|
|
|
65
70
|
assert value.ok, f"Error reading {source}: {value}"
|
|
66
71
|
|
|
67
72
|
scalar = value.element_count == 1
|
|
68
|
-
dtype = dtype or dbr_to_dtype[value.datatype]
|
|
73
|
+
dtype = dtype or dbr_to_dtype[value.datatype] # type: ignore
|
|
69
74
|
|
|
70
75
|
dtype_numpy = np.dtype(dbr.DbrCodeToType[value.datatype].dtype).descr[0][1]
|
|
71
76
|
|
|
72
77
|
d = DataKey(
|
|
73
78
|
source=source,
|
|
74
79
|
dtype=dtype if scalar else "array",
|
|
75
|
-
|
|
80
|
+
# Ignore until https://github.com/bluesky/event-model/issues/308
|
|
81
|
+
dtype_numpy=dtype_numpy, # type: ignore
|
|
76
82
|
# strictly value.element_count >= len(value)
|
|
77
83
|
shape=[] if scalar else [len(value)],
|
|
78
84
|
)
|
|
@@ -82,10 +88,10 @@ def _data_key_from_augmented_value(
|
|
|
82
88
|
d[key] = attr
|
|
83
89
|
|
|
84
90
|
if choices is not None:
|
|
85
|
-
d["choices"] = choices
|
|
91
|
+
d["choices"] = choices # type: ignore
|
|
86
92
|
|
|
87
93
|
if limits := _limits_from_augmented_value(value):
|
|
88
|
-
d["limits"] = limits
|
|
94
|
+
d["limits"] = limits # type: ignore
|
|
89
95
|
|
|
90
96
|
return d
|
|
91
97
|
|
|
@@ -108,8 +114,8 @@ def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
|
108
114
|
|
|
109
115
|
@dataclass
|
|
110
116
|
class CaConverter:
|
|
111
|
-
read_dbr:
|
|
112
|
-
write_dbr:
|
|
117
|
+
read_dbr: Dbr | None
|
|
118
|
+
write_dbr: Dbr | None
|
|
113
119
|
|
|
114
120
|
def write_value(self, value) -> Any:
|
|
115
121
|
return value
|
|
@@ -118,9 +124,9 @@ class CaConverter:
|
|
|
118
124
|
# for channel access ca_xxx classes, this
|
|
119
125
|
# invokes __pos__ operator to return an instance of
|
|
120
126
|
# the builtin base class
|
|
121
|
-
return +value
|
|
127
|
+
return +value # type: ignore
|
|
122
128
|
|
|
123
|
-
def reading(self, value: AugmentedValue):
|
|
129
|
+
def reading(self, value: AugmentedValue) -> Reading:
|
|
124
130
|
return {
|
|
125
131
|
"value": self.value(value),
|
|
126
132
|
"timestamp": value.timestamp,
|
|
@@ -155,14 +161,14 @@ class CaEnumConverter(CaConverter):
|
|
|
155
161
|
|
|
156
162
|
choices: dict[str, str]
|
|
157
163
|
|
|
158
|
-
def write_value(self, value:
|
|
164
|
+
def write_value(self, value: Enum | str):
|
|
159
165
|
if isinstance(value, Enum):
|
|
160
166
|
return value.value
|
|
161
167
|
else:
|
|
162
168
|
return value
|
|
163
169
|
|
|
164
170
|
def value(self, value: AugmentedValue):
|
|
165
|
-
return self.choices[value]
|
|
171
|
+
return self.choices[value] # type: ignore
|
|
166
172
|
|
|
167
173
|
def get_datakey(self, value: AugmentedValue) -> DataKey:
|
|
168
174
|
# Sometimes DBR_TYPE returns as String, must pass choices still
|
|
@@ -184,7 +190,7 @@ class DisconnectedCaConverter(CaConverter):
|
|
|
184
190
|
|
|
185
191
|
|
|
186
192
|
def make_converter(
|
|
187
|
-
datatype:
|
|
193
|
+
datatype: type | None, values: dict[str, AugmentedValue]
|
|
188
194
|
) -> CaConverter:
|
|
189
195
|
pv = list(values)[0]
|
|
190
196
|
pv_dbr = get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
|
|
@@ -200,7 +206,7 @@ def make_converter(
|
|
|
200
206
|
raise TypeError(f"{pv} has type [str] not {datatype.__name__}")
|
|
201
207
|
return CaArrayConverter(pv_dbr, None)
|
|
202
208
|
elif is_array:
|
|
203
|
-
pv_dtype = get_unique({k: v.dtype for k, v in values.items()}, "dtypes")
|
|
209
|
+
pv_dtype = get_unique({k: v.dtype for k, v in values.items()}, "dtypes") # type: ignore
|
|
204
210
|
# This is an array
|
|
205
211
|
if datatype:
|
|
206
212
|
# Check we wanted an array of this type
|
|
@@ -209,9 +215,10 @@ def make_converter(
|
|
|
209
215
|
raise TypeError(f"{pv} has type [{pv_dtype}] not {datatype.__name__}")
|
|
210
216
|
if dtype != pv_dtype:
|
|
211
217
|
raise TypeError(f"{pv} has type [{pv_dtype}] not [{dtype}]")
|
|
212
|
-
return CaArrayConverter(pv_dbr, None)
|
|
218
|
+
return CaArrayConverter(pv_dbr, None) # type: ignore
|
|
213
219
|
elif pv_dbr == dbr.DBR_ENUM and datatype is bool:
|
|
214
|
-
# Database can't do bools, so are often representated as enums,
|
|
220
|
+
# Database can't do bools, so are often representated as enums,
|
|
221
|
+
# CA can do int
|
|
215
222
|
pv_choices_len = get_unique(
|
|
216
223
|
{k: len(v.enums) for k, v in values.items()}, "number of choices"
|
|
217
224
|
)
|
|
@@ -240,7 +247,7 @@ def make_converter(
|
|
|
240
247
|
f"{pv} has type {type(value).__name__.replace('ca_', '')} "
|
|
241
248
|
+ f"not {datatype.__name__}"
|
|
242
249
|
)
|
|
243
|
-
|
|
250
|
+
return CaConverter(pv_dbr, None) # type: ignore
|
|
244
251
|
|
|
245
252
|
|
|
246
253
|
_tried_pyepics = False
|
|
@@ -256,13 +263,36 @@ def _use_pyepics_context_if_imported():
|
|
|
256
263
|
|
|
257
264
|
|
|
258
265
|
class CaSignalBackend(SignalBackend[T]):
|
|
259
|
-
|
|
266
|
+
_ALLOWED_DATATYPES = (
|
|
267
|
+
bool,
|
|
268
|
+
int,
|
|
269
|
+
float,
|
|
270
|
+
str,
|
|
271
|
+
Sequence,
|
|
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):
|
|
260
288
|
self.datatype = datatype
|
|
289
|
+
if not CaSignalBackend.datatype_allowed(self.datatype):
|
|
290
|
+
raise TypeError(f"Given datatype {self.datatype} unsupported in CA.")
|
|
261
291
|
self.read_pv = read_pv
|
|
262
292
|
self.write_pv = write_pv
|
|
263
|
-
self.initial_values:
|
|
293
|
+
self.initial_values: dict[str, AugmentedValue] = {}
|
|
264
294
|
self.converter: CaConverter = DisconnectedCaConverter(None, None)
|
|
265
|
-
self.subscription:
|
|
295
|
+
self.subscription: Subscription | None = None
|
|
266
296
|
|
|
267
297
|
def source(self, name: str):
|
|
268
298
|
return f"ca://{self.read_pv}"
|
|
@@ -289,7 +319,7 @@ class CaSignalBackend(SignalBackend[T]):
|
|
|
289
319
|
await self._store_initial_value(self.read_pv, timeout=timeout)
|
|
290
320
|
self.converter = make_converter(self.datatype, self.initial_values)
|
|
291
321
|
|
|
292
|
-
async def put(self, value:
|
|
322
|
+
async def put(self, value: T | None, wait=True, timeout=None):
|
|
293
323
|
if value is None:
|
|
294
324
|
write_value = self.initial_values[self.write_pv]
|
|
295
325
|
else:
|
|
@@ -331,7 +361,7 @@ class CaSignalBackend(SignalBackend[T]):
|
|
|
331
361
|
)
|
|
332
362
|
return self.converter.value(value)
|
|
333
363
|
|
|
334
|
-
def set_callback(self, callback:
|
|
364
|
+
def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
|
|
335
365
|
if callback:
|
|
336
366
|
assert (
|
|
337
367
|
not self.subscription
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Dict, Optional, Tuple, Type
|
|
4
3
|
|
|
5
4
|
from typing_extensions import TypedDict
|
|
6
5
|
|
|
@@ -16,9 +15,6 @@ class LimitPair(TypedDict):
|
|
|
16
15
|
high: float | None
|
|
17
16
|
low: float | None
|
|
18
17
|
|
|
19
|
-
def __bool__(self) -> bool:
|
|
20
|
-
return self.low is None and self.high is None
|
|
21
|
-
|
|
22
18
|
|
|
23
19
|
class Limits(TypedDict):
|
|
24
20
|
alarm: LimitPair
|
|
@@ -26,15 +22,12 @@ class Limits(TypedDict):
|
|
|
26
22
|
display: LimitPair
|
|
27
23
|
warning: LimitPair
|
|
28
24
|
|
|
29
|
-
def __bool__(self) -> bool:
|
|
30
|
-
return any(self.alarm, self.control, self.display, self.warning)
|
|
31
|
-
|
|
32
25
|
|
|
33
26
|
def get_supported_values(
|
|
34
27
|
pv: str,
|
|
35
|
-
datatype:
|
|
36
|
-
pv_choices:
|
|
37
|
-
) ->
|
|
28
|
+
datatype: type[str] | None,
|
|
29
|
+
pv_choices: tuple[str, ...],
|
|
30
|
+
) -> dict[str, str]:
|
|
38
31
|
if inspect.isclass(datatype) and issubclass(datatype, RuntimeSubsetEnum):
|
|
39
32
|
if not set(datatype.choices).issubset(set(pv_choices)):
|
|
40
33
|
raise TypeError(
|
|
@@ -4,22 +4,25 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
def _make_unavailable_class(error: Exception) -> type:
|
|
9
|
+
class TransportNotAvailable:
|
|
10
|
+
def __init__(*args, **kwargs):
|
|
11
|
+
raise NotImplementedError("Transport not available") from error
|
|
12
|
+
|
|
13
|
+
return TransportNotAvailable
|
|
14
|
+
|
|
15
|
+
|
|
7
16
|
try:
|
|
8
17
|
from ._aioca import CaSignalBackend
|
|
9
18
|
except ImportError as ca_error:
|
|
10
|
-
|
|
11
|
-
class CaSignalBackend: # type: ignore
|
|
12
|
-
def __init__(*args, ca_error=ca_error, **kwargs):
|
|
13
|
-
raise NotImplementedError("CA support not available") from ca_error
|
|
19
|
+
CaSignalBackend = _make_unavailable_class(ca_error)
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
try:
|
|
17
23
|
from ._p4p import PvaSignalBackend
|
|
18
24
|
except ImportError as pva_error:
|
|
19
|
-
|
|
20
|
-
class PvaSignalBackend: # type: ignore
|
|
21
|
-
def __init__(*args, pva_error=pva_error, **kwargs):
|
|
22
|
-
raise NotImplementedError("PVA support not available") from pva_error
|
|
25
|
+
PvaSignalBackend = _make_unavailable_class(pva_error)
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
class _EpicsTransport(Enum):
|
ophyd_async/epics/signal/_p4p.py
CHANGED
|
@@ -3,14 +3,19 @@ import atexit
|
|
|
3
3
|
import inspect
|
|
4
4
|
import logging
|
|
5
5
|
import time
|
|
6
|
+
from collections.abc import Sequence
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from enum import Enum
|
|
8
9
|
from math import isnan, nan
|
|
9
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, get_origin
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
import numpy as np
|
|
13
|
+
from bluesky.protocols import Reading
|
|
14
|
+
from event_model import DataKey
|
|
15
|
+
from event_model.documents.event_descriptor import Dtype
|
|
12
16
|
from p4p import Value
|
|
13
17
|
from p4p.client.asyncio import Context, Subscription
|
|
18
|
+
from pydantic import BaseModel
|
|
14
19
|
|
|
15
20
|
from ophyd_async.core import (
|
|
16
21
|
DEFAULT_TIMEOUT,
|
|
@@ -21,13 +26,14 @@ from ophyd_async.core import (
|
|
|
21
26
|
T,
|
|
22
27
|
get_dtype,
|
|
23
28
|
get_unique,
|
|
29
|
+
is_pydantic_model,
|
|
24
30
|
wait_for_connection,
|
|
25
31
|
)
|
|
26
32
|
|
|
27
33
|
from ._common import LimitPair, Limits, common_meta, get_supported_values
|
|
28
34
|
|
|
29
35
|
# https://mdavidsaver.github.io/p4p/values.html
|
|
30
|
-
specifier_to_dtype:
|
|
36
|
+
specifier_to_dtype: dict[str, Dtype] = {
|
|
31
37
|
"?": "integer", # bool
|
|
32
38
|
"b": "integer", # int8
|
|
33
39
|
"B": "integer", # uint8
|
|
@@ -42,7 +48,7 @@ specifier_to_dtype: Dict[str, Dtype] = {
|
|
|
42
48
|
"s": "string",
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
specifier_to_np_dtype:
|
|
51
|
+
specifier_to_np_dtype: dict[str, str] = {
|
|
46
52
|
"?": "<i2", # bool
|
|
47
53
|
"b": "|i1", # int8
|
|
48
54
|
"B": "|u1", # uint8
|
|
@@ -62,9 +68,9 @@ def _data_key_from_value(
|
|
|
62
68
|
source: str,
|
|
63
69
|
value: Value,
|
|
64
70
|
*,
|
|
65
|
-
shape:
|
|
66
|
-
choices:
|
|
67
|
-
dtype:
|
|
71
|
+
shape: list[int] | None = None,
|
|
72
|
+
choices: list[str] | None = None,
|
|
73
|
+
dtype: Dtype | None = None,
|
|
68
74
|
) -> DataKey:
|
|
69
75
|
"""
|
|
70
76
|
Args:
|
|
@@ -105,7 +111,8 @@ def _data_key_from_value(
|
|
|
105
111
|
d = DataKey(
|
|
106
112
|
source=source,
|
|
107
113
|
dtype=dtype,
|
|
108
|
-
|
|
114
|
+
# type ignore until https://github.com/bluesky/event-model/issues/308
|
|
115
|
+
dtype_numpy=dtype_numpy, # type: ignore
|
|
109
116
|
shape=shape,
|
|
110
117
|
)
|
|
111
118
|
if display_data is not None:
|
|
@@ -115,10 +122,12 @@ def _data_key_from_value(
|
|
|
115
122
|
d[key] = attr
|
|
116
123
|
|
|
117
124
|
if choices is not None:
|
|
118
|
-
|
|
125
|
+
# type ignore until https://github.com/bluesky/event-model/issues/309
|
|
126
|
+
d["choices"] = choices # type: ignore
|
|
119
127
|
|
|
120
128
|
if limits := _limits_from_value(value):
|
|
121
|
-
|
|
129
|
+
# type ignore until https://github.com/bluesky/event-model/issues/309
|
|
130
|
+
d["limits"] = limits # type: ignore
|
|
122
131
|
|
|
123
132
|
return d
|
|
124
133
|
|
|
@@ -149,7 +158,7 @@ class PvaConverter:
|
|
|
149
158
|
def value(self, value):
|
|
150
159
|
return value["value"]
|
|
151
160
|
|
|
152
|
-
def reading(self, value):
|
|
161
|
+
def reading(self, value) -> Reading:
|
|
153
162
|
ts = value["timeStamp"]
|
|
154
163
|
sv = value["alarm"]["severity"]
|
|
155
164
|
return {
|
|
@@ -161,13 +170,13 @@ class PvaConverter:
|
|
|
161
170
|
def get_datakey(self, source: str, value) -> DataKey:
|
|
162
171
|
return _data_key_from_value(source, value)
|
|
163
172
|
|
|
164
|
-
def metadata_fields(self) ->
|
|
173
|
+
def metadata_fields(self) -> list[str]:
|
|
165
174
|
"""
|
|
166
175
|
Fields to request from PVA for metadata.
|
|
167
176
|
"""
|
|
168
177
|
return ["alarm", "timeStamp"]
|
|
169
178
|
|
|
170
|
-
def value_fields(self) ->
|
|
179
|
+
def value_fields(self) -> list[str]:
|
|
171
180
|
"""
|
|
172
181
|
Fields to request from PVA for the value.
|
|
173
182
|
"""
|
|
@@ -182,11 +191,11 @@ class PvaArrayConverter(PvaConverter):
|
|
|
182
191
|
|
|
183
192
|
|
|
184
193
|
class PvaNDArrayConverter(PvaConverter):
|
|
185
|
-
def metadata_fields(self) ->
|
|
194
|
+
def metadata_fields(self) -> list[str]:
|
|
186
195
|
return super().metadata_fields() + ["dimension"]
|
|
187
196
|
|
|
188
|
-
def _get_dimensions(self, value) ->
|
|
189
|
-
dimensions:
|
|
197
|
+
def _get_dimensions(self, value) -> list[int]:
|
|
198
|
+
dimensions: list[Value] = value["dimension"]
|
|
190
199
|
dims = [dim.size for dim in dimensions]
|
|
191
200
|
# Note: dimensions in NTNDArray are in fortran-like order
|
|
192
201
|
# with first index changing fastest.
|
|
@@ -221,7 +230,7 @@ class PvaEnumConverter(PvaConverter):
|
|
|
221
230
|
def __init__(self, choices: dict[str, str]):
|
|
222
231
|
self.choices = tuple(choices.values())
|
|
223
232
|
|
|
224
|
-
def write_value(self, value:
|
|
233
|
+
def write_value(self, value: Enum | str):
|
|
225
234
|
if isinstance(value, Enum):
|
|
226
235
|
return value.value
|
|
227
236
|
else:
|
|
@@ -250,11 +259,24 @@ class PvaTableConverter(PvaConverter):
|
|
|
250
259
|
|
|
251
260
|
def get_datakey(self, source: str, value) -> DataKey:
|
|
252
261
|
# This is wrong, but defer until we know how to actually describe a table
|
|
253
|
-
return _data_key_from_value(source, value, dtype="object")
|
|
262
|
+
return _data_key_from_value(source, value, dtype="object") # type: ignore
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class PvaPydanticModelConverter(PvaConverter):
|
|
266
|
+
def __init__(self, datatype: BaseModel):
|
|
267
|
+
self.datatype = datatype
|
|
268
|
+
|
|
269
|
+
def value(self, value: Value):
|
|
270
|
+
return self.datatype(**value.todict()) # type: ignore
|
|
271
|
+
|
|
272
|
+
def write_value(self, value: BaseModel | dict[str, Any]):
|
|
273
|
+
if isinstance(value, self.datatype): # type: ignore
|
|
274
|
+
return value.model_dump(mode="python") # type: ignore
|
|
275
|
+
return value
|
|
254
276
|
|
|
255
277
|
|
|
256
278
|
class PvaDictConverter(PvaConverter):
|
|
257
|
-
def reading(self, value):
|
|
279
|
+
def reading(self, value) -> Reading:
|
|
258
280
|
ts = time.time()
|
|
259
281
|
value = value.todict()
|
|
260
282
|
# Alarm severity is vacuously 0 for a table
|
|
@@ -266,13 +288,13 @@ class PvaDictConverter(PvaConverter):
|
|
|
266
288
|
def get_datakey(self, source: str, value) -> DataKey:
|
|
267
289
|
raise NotImplementedError("Describing Dict signals not currently supported")
|
|
268
290
|
|
|
269
|
-
def metadata_fields(self) ->
|
|
291
|
+
def metadata_fields(self) -> list[str]:
|
|
270
292
|
"""
|
|
271
293
|
Fields to request from PVA for metadata.
|
|
272
294
|
"""
|
|
273
295
|
return []
|
|
274
296
|
|
|
275
|
-
def value_fields(self) ->
|
|
297
|
+
def value_fields(self) -> list[str]:
|
|
276
298
|
"""
|
|
277
299
|
Fields to request from PVA for the value.
|
|
278
300
|
"""
|
|
@@ -284,7 +306,7 @@ class DisconnectedPvaConverter(PvaConverter):
|
|
|
284
306
|
raise NotImplementedError("No PV has been set as connect() has not been called")
|
|
285
307
|
|
|
286
308
|
|
|
287
|
-
def make_converter(datatype:
|
|
309
|
+
def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverter:
|
|
288
310
|
pv = list(values)[0]
|
|
289
311
|
typeid = get_unique({k: v.getID() for k, v in values.items()}, "typeids")
|
|
290
312
|
typ = get_unique(
|
|
@@ -333,7 +355,7 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
|
|
|
333
355
|
and issubclass(datatype, RuntimeSubsetEnum)
|
|
334
356
|
):
|
|
335
357
|
return PvaEnumConverter(
|
|
336
|
-
get_supported_values(pv, datatype, datatype.choices)
|
|
358
|
+
get_supported_values(pv, datatype, datatype.choices) # type: ignore
|
|
337
359
|
)
|
|
338
360
|
elif datatype and not issubclass(typ, datatype):
|
|
339
361
|
# Allow int signals to represent float records when prec is 0
|
|
@@ -348,6 +370,8 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
|
|
|
348
370
|
raise TypeError(f"{pv} has type {typ.__name__} not {datatype.__name__}")
|
|
349
371
|
return PvaConverter()
|
|
350
372
|
elif "NTTable" in typeid:
|
|
373
|
+
if is_pydantic_model(datatype):
|
|
374
|
+
return PvaPydanticModelConverter(datatype) # type: ignore
|
|
351
375
|
return PvaTableConverter()
|
|
352
376
|
elif "structure" in typeid:
|
|
353
377
|
return PvaDictConverter()
|
|
@@ -356,15 +380,40 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
|
|
|
356
380
|
|
|
357
381
|
|
|
358
382
|
class PvaSignalBackend(SignalBackend[T]):
|
|
359
|
-
_ctxt:
|
|
383
|
+
_ctxt: Context | None = None
|
|
384
|
+
|
|
385
|
+
_ALLOWED_DATATYPES = (
|
|
386
|
+
bool,
|
|
387
|
+
int,
|
|
388
|
+
float,
|
|
389
|
+
str,
|
|
390
|
+
Sequence,
|
|
391
|
+
np.ndarray,
|
|
392
|
+
Enum,
|
|
393
|
+
RuntimeSubsetEnum,
|
|
394
|
+
BaseModel,
|
|
395
|
+
dict,
|
|
396
|
+
)
|
|
360
397
|
|
|
361
|
-
|
|
398
|
+
@classmethod
|
|
399
|
+
def datatype_allowed(cls, dtype: Any) -> bool:
|
|
400
|
+
stripped_origin = get_origin(dtype) or dtype
|
|
401
|
+
if dtype is None:
|
|
402
|
+
return True
|
|
403
|
+
return inspect.isclass(stripped_origin) and issubclass(
|
|
404
|
+
stripped_origin, cls._ALLOWED_DATATYPES
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def __init__(self, datatype: type[T] | None, read_pv: str, write_pv: str):
|
|
362
408
|
self.datatype = datatype
|
|
409
|
+
if not PvaSignalBackend.datatype_allowed(self.datatype):
|
|
410
|
+
raise TypeError(f"Given datatype {self.datatype} unsupported in PVA.")
|
|
411
|
+
|
|
363
412
|
self.read_pv = read_pv
|
|
364
413
|
self.write_pv = write_pv
|
|
365
|
-
self.initial_values:
|
|
414
|
+
self.initial_values: dict[str, Any] = {}
|
|
366
415
|
self.converter: PvaConverter = DisconnectedPvaConverter()
|
|
367
|
-
self.subscription:
|
|
416
|
+
self.subscription: Subscription | None = None
|
|
368
417
|
|
|
369
418
|
def source(self, name: str):
|
|
370
419
|
return f"pva://{self.read_pv}"
|
|
@@ -404,7 +453,7 @@ class PvaSignalBackend(SignalBackend[T]):
|
|
|
404
453
|
await self._store_initial_value(self.read_pv, timeout=timeout)
|
|
405
454
|
self.converter = make_converter(self.datatype, self.initial_values)
|
|
406
455
|
|
|
407
|
-
async def put(self, value:
|
|
456
|
+
async def put(self, value: T | None, wait=True, timeout=None):
|
|
408
457
|
if value is None:
|
|
409
458
|
write_value = self.initial_values[self.write_pv]
|
|
410
459
|
else:
|
|
@@ -424,7 +473,7 @@ class PvaSignalBackend(SignalBackend[T]):
|
|
|
424
473
|
value = await self.ctxt.get(self.read_pv)
|
|
425
474
|
return self.converter.get_datakey(source, value)
|
|
426
475
|
|
|
427
|
-
def _pva_request_string(self, fields:
|
|
476
|
+
def _pva_request_string(self, fields: list[str]) -> str:
|
|
428
477
|
"""
|
|
429
478
|
Converts a list of requested fields into a PVA request string which can be
|
|
430
479
|
passed to p4p.
|
|
@@ -447,7 +496,7 @@ class PvaSignalBackend(SignalBackend[T]):
|
|
|
447
496
|
value = await self.ctxt.get(self.write_pv, "field(value)")
|
|
448
497
|
return self.converter.value(value)
|
|
449
498
|
|
|
450
|
-
def set_callback(self, callback:
|
|
499
|
+
def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
|
|
451
500
|
if callback:
|
|
452
501
|
assert (
|
|
453
502
|
not self.subscription
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Optional, Tuple, Type
|
|
6
|
-
|
|
7
5
|
from ophyd_async.core import (
|
|
8
6
|
SignalBackend,
|
|
9
7
|
SignalR,
|
|
@@ -19,7 +17,7 @@ from ._epics_transport import _EpicsTransport
|
|
|
19
17
|
_default_epics_transport = _EpicsTransport.ca
|
|
20
18
|
|
|
21
19
|
|
|
22
|
-
def _transport_pv(pv: str) ->
|
|
20
|
+
def _transport_pv(pv: str) -> tuple[_EpicsTransport, str]:
|
|
23
21
|
split = pv.split("://", 1)
|
|
24
22
|
if len(split) > 1:
|
|
25
23
|
# We got something like pva://mydevice, so use specified comms mode
|
|
@@ -32,7 +30,7 @@ def _transport_pv(pv: str) -> Tuple[_EpicsTransport, str]:
|
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
def _epics_signal_backend(
|
|
35
|
-
datatype:
|
|
33
|
+
datatype: type[T] | None, read_pv: str, write_pv: str
|
|
36
34
|
) -> SignalBackend[T]:
|
|
37
35
|
"""Create an epics signal backend."""
|
|
38
36
|
r_transport, r_pv = _transport_pv(read_pv)
|
|
@@ -42,7 +40,7 @@ def _epics_signal_backend(
|
|
|
42
40
|
|
|
43
41
|
|
|
44
42
|
def epics_signal_rw(
|
|
45
|
-
datatype:
|
|
43
|
+
datatype: type[T], read_pv: str, write_pv: str | None = None, name: str = ""
|
|
46
44
|
) -> SignalRW[T]:
|
|
47
45
|
"""Create a `SignalRW` backed by 1 or 2 EPICS PVs
|
|
48
46
|
|
|
@@ -60,7 +58,7 @@ def epics_signal_rw(
|
|
|
60
58
|
|
|
61
59
|
|
|
62
60
|
def epics_signal_rw_rbv(
|
|
63
|
-
datatype:
|
|
61
|
+
datatype: type[T], write_pv: str, read_suffix: str = "_RBV", name: str = ""
|
|
64
62
|
) -> SignalRW[T]:
|
|
65
63
|
"""Create a `SignalRW` backed by 1 or 2 EPICS PVs, with a suffix on the readback pv
|
|
66
64
|
|
|
@@ -76,7 +74,7 @@ def epics_signal_rw_rbv(
|
|
|
76
74
|
return epics_signal_rw(datatype, f"{write_pv}{read_suffix}", write_pv, name)
|
|
77
75
|
|
|
78
76
|
|
|
79
|
-
def epics_signal_r(datatype:
|
|
77
|
+
def epics_signal_r(datatype: type[T], read_pv: str, name: str = "") -> SignalR[T]:
|
|
80
78
|
"""Create a `SignalR` backed by 1 EPICS PV
|
|
81
79
|
|
|
82
80
|
Parameters
|
|
@@ -90,7 +88,7 @@ def epics_signal_r(datatype: Type[T], read_pv: str, name: str = "") -> SignalR[T
|
|
|
90
88
|
return SignalR(backend, name=name)
|
|
91
89
|
|
|
92
90
|
|
|
93
|
-
def epics_signal_w(datatype:
|
|
91
|
+
def epics_signal_w(datatype: type[T], write_pv: str, name: str = "") -> SignalW[T]:
|
|
94
92
|
"""Create a `SignalW` backed by 1 EPICS PVs
|
|
95
93
|
|
|
96
94
|
Parameters
|
|
@@ -15,10 +15,7 @@ from ._table import (
|
|
|
15
15
|
DatasetTable,
|
|
16
16
|
PandaHdf5DatasetType,
|
|
17
17
|
SeqTable,
|
|
18
|
-
SeqTableRow,
|
|
19
18
|
SeqTrigger,
|
|
20
|
-
seq_table_from_arrays,
|
|
21
|
-
seq_table_from_rows,
|
|
22
19
|
)
|
|
23
20
|
from ._trigger import (
|
|
24
21
|
PcompInfo,
|
|
@@ -45,10 +42,7 @@ __all__ = [
|
|
|
45
42
|
"DatasetTable",
|
|
46
43
|
"PandaHdf5DatasetType",
|
|
47
44
|
"SeqTable",
|
|
48
|
-
"SeqTableRow",
|
|
49
45
|
"SeqTrigger",
|
|
50
|
-
"seq_table_from_arrays",
|
|
51
|
-
"seq_table_from_rows",
|
|
52
46
|
"PcompInfo",
|
|
53
47
|
"SeqTableInfo",
|
|
54
48
|
"StaticPcompTriggerLogic",
|
|
@@ -7,6 +7,12 @@ from ophyd_async.core import Device, DeviceVector, SignalR, SignalRW, SubsetEnum
|
|
|
7
7
|
from ._table import DatasetTable, SeqTable
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class CaptureMode(str, Enum):
|
|
11
|
+
FIRST_N = "FIRST_N"
|
|
12
|
+
LAST_N = "LAST_N"
|
|
13
|
+
FOREVER = "FOREVER"
|
|
14
|
+
|
|
15
|
+
|
|
10
16
|
class DataBlock(Device):
|
|
11
17
|
# In future we may decide to make hdf_* optional
|
|
12
18
|
hdf_directory: SignalRW[str]
|
|
@@ -15,6 +21,7 @@ class DataBlock(Device):
|
|
|
15
21
|
num_captured: SignalR[int]
|
|
16
22
|
create_directory: SignalRW[int]
|
|
17
23
|
directory_exists: SignalR[bool]
|
|
24
|
+
capture_mode: SignalRW[CaptureMode]
|
|
18
25
|
capture: SignalRW[bool]
|
|
19
26
|
flush_period: SignalRW[float]
|
|
20
27
|
datasets: SignalR[DatasetTable]
|