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,97 +1,164 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
Any,
|
|
5
|
-
ClassVar,
|
|
6
|
-
Generic,
|
|
7
|
-
Literal,
|
|
8
|
-
)
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import Generic, TypedDict, TypeVar, get_origin
|
|
9
4
|
|
|
5
|
+
import numpy as np
|
|
10
6
|
from bluesky.protocols import Reading
|
|
11
|
-
from event_model import DataKey
|
|
12
|
-
|
|
13
|
-
from .
|
|
7
|
+
from event_model import DataKey, Dtype, Limits
|
|
8
|
+
|
|
9
|
+
from ._table import Table
|
|
10
|
+
from ._utils import Callback, StrictEnum, T
|
|
11
|
+
|
|
12
|
+
DTypeScalar_co = TypeVar("DTypeScalar_co", covariant=True, bound=np.generic)
|
|
13
|
+
Array1D = np.ndarray[tuple[int], np.dtype[DTypeScalar_co]]
|
|
14
|
+
Primitive = bool | int | float | str
|
|
15
|
+
# NOTE: if you change this union then update the docs to match
|
|
16
|
+
SignalDatatype = (
|
|
17
|
+
Primitive
|
|
18
|
+
| Array1D[np.bool_]
|
|
19
|
+
| Array1D[np.int8]
|
|
20
|
+
| Array1D[np.uint8]
|
|
21
|
+
| Array1D[np.int16]
|
|
22
|
+
| Array1D[np.uint16]
|
|
23
|
+
| Array1D[np.int32]
|
|
24
|
+
| Array1D[np.uint32]
|
|
25
|
+
| Array1D[np.int64]
|
|
26
|
+
| Array1D[np.uint64]
|
|
27
|
+
| Array1D[np.float32]
|
|
28
|
+
| Array1D[np.float64]
|
|
29
|
+
| np.ndarray
|
|
30
|
+
| StrictEnum
|
|
31
|
+
| Sequence[str]
|
|
32
|
+
| Sequence[StrictEnum]
|
|
33
|
+
| Table
|
|
34
|
+
)
|
|
35
|
+
# TODO: These typevars will not be needed when we drop python 3.11
|
|
36
|
+
# as you can do MyConverter[SignalType: SignalTypeUnion]:
|
|
37
|
+
# rather than MyConverter(Generic[SignalType])
|
|
38
|
+
PrimitiveT = TypeVar("PrimitiveT", bound=Primitive)
|
|
39
|
+
SignalDatatypeT = TypeVar("SignalDatatypeT", bound=SignalDatatype)
|
|
40
|
+
SignalDatatypeV = TypeVar("SignalDatatypeV", bound=SignalDatatype)
|
|
41
|
+
EnumT = TypeVar("EnumT", bound=StrictEnum)
|
|
42
|
+
TableT = TypeVar("TableT", bound=Table)
|
|
14
43
|
|
|
15
44
|
|
|
16
|
-
class SignalBackend(Generic[
|
|
45
|
+
class SignalBackend(Generic[SignalDatatypeT]):
|
|
17
46
|
"""A read/write/monitor backend for a Signals"""
|
|
18
47
|
|
|
19
|
-
|
|
20
|
-
|
|
48
|
+
def __init__(self, datatype: type[SignalDatatypeT] | None):
|
|
49
|
+
self.datatype = datatype
|
|
21
50
|
|
|
22
|
-
@classmethod
|
|
23
51
|
@abstractmethod
|
|
24
|
-
def
|
|
25
|
-
"""
|
|
52
|
+
def source(self, name: str, read: bool) -> str:
|
|
53
|
+
"""Return source of signal.
|
|
26
54
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def source(self, name: str) -> str:
|
|
30
|
-
"""Return source of signal. Signals may pass a name to the backend, which can be
|
|
31
|
-
used or discarded."""
|
|
55
|
+
Signals may pass a name to the backend, which can be used or discarded.
|
|
56
|
+
"""
|
|
32
57
|
|
|
33
58
|
@abstractmethod
|
|
34
|
-
async def connect(self, timeout: float
|
|
59
|
+
async def connect(self, timeout: float):
|
|
35
60
|
"""Connect to underlying hardware"""
|
|
36
61
|
|
|
37
62
|
@abstractmethod
|
|
38
|
-
async def put(self, value:
|
|
39
|
-
"""Put a value to the PV, if wait then wait for completion
|
|
63
|
+
async def put(self, value: SignalDatatypeT | None, wait: bool):
|
|
64
|
+
"""Put a value to the PV, if wait then wait for completion"""
|
|
40
65
|
|
|
41
66
|
@abstractmethod
|
|
42
67
|
async def get_datakey(self, source: str) -> DataKey:
|
|
43
68
|
"""Metadata like source, dtype, shape, precision, units"""
|
|
44
69
|
|
|
45
70
|
@abstractmethod
|
|
46
|
-
async def get_reading(self) -> Reading:
|
|
71
|
+
async def get_reading(self) -> Reading[SignalDatatypeT]:
|
|
47
72
|
"""The current value, timestamp and severity"""
|
|
48
73
|
|
|
49
74
|
@abstractmethod
|
|
50
|
-
async def get_value(self) ->
|
|
75
|
+
async def get_value(self) -> SignalDatatypeT:
|
|
51
76
|
"""The current value"""
|
|
52
77
|
|
|
53
78
|
@abstractmethod
|
|
54
|
-
async def get_setpoint(self) ->
|
|
79
|
+
async def get_setpoint(self) -> SignalDatatypeT:
|
|
55
80
|
"""The point that a signal was requested to move to."""
|
|
56
81
|
|
|
57
82
|
@abstractmethod
|
|
58
|
-
def set_callback(self, callback:
|
|
83
|
+
def set_callback(self, callback: Callback[T] | None) -> None:
|
|
59
84
|
"""Observe changes to the current value, timestamp and severity"""
|
|
60
85
|
|
|
61
86
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
87
|
+
_primitive_dtype: dict[type[Primitive], Dtype] = {
|
|
88
|
+
bool: "boolean",
|
|
89
|
+
int: "integer",
|
|
90
|
+
float: "number",
|
|
91
|
+
str: "string",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SignalMetadata(TypedDict, total=False):
|
|
96
|
+
limits: Limits
|
|
97
|
+
choices: list[str]
|
|
98
|
+
precision: int
|
|
99
|
+
units: str
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _datakey_dtype(datatype: type[SignalDatatype]) -> Dtype:
|
|
103
|
+
if (
|
|
104
|
+
datatype is np.ndarray
|
|
105
|
+
or get_origin(datatype) in (Sequence, np.ndarray)
|
|
106
|
+
or issubclass(datatype, Table)
|
|
107
|
+
):
|
|
108
|
+
return "array"
|
|
109
|
+
elif issubclass(datatype, StrictEnum):
|
|
110
|
+
return "string"
|
|
111
|
+
elif issubclass(datatype, Primitive):
|
|
112
|
+
return _primitive_dtype[datatype]
|
|
113
|
+
else:
|
|
114
|
+
raise TypeError(f"Can't make dtype for {datatype}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _datakey_dtype_numpy(
|
|
118
|
+
datatype: type[SignalDatatypeT], value: SignalDatatypeT
|
|
119
|
+
) -> np.dtype:
|
|
120
|
+
if isinstance(value, np.ndarray):
|
|
121
|
+
# The value already has a dtype, use that
|
|
122
|
+
return value.dtype
|
|
123
|
+
elif (
|
|
124
|
+
get_origin(datatype) is Sequence
|
|
125
|
+
or datatype is str
|
|
126
|
+
or issubclass(datatype, StrictEnum)
|
|
127
|
+
):
|
|
128
|
+
# TODO: use np.dtypes.StringDType when we can use in structured arrays
|
|
129
|
+
# https://github.com/numpy/numpy/issues/25693
|
|
130
|
+
return np.dtype("S40")
|
|
131
|
+
elif isinstance(value, Table):
|
|
132
|
+
return value.numpy_dtype()
|
|
133
|
+
elif issubclass(datatype, Primitive):
|
|
134
|
+
return np.dtype(datatype)
|
|
135
|
+
else:
|
|
136
|
+
raise TypeError(f"Can't make dtype_numpy for {datatype}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _datakey_shape(value: SignalDatatype) -> list[int]:
|
|
140
|
+
if type(value) in _primitive_dtype or isinstance(value, StrictEnum):
|
|
141
|
+
return []
|
|
142
|
+
elif isinstance(value, np.ndarray):
|
|
143
|
+
return list(value.shape)
|
|
144
|
+
elif isinstance(value, Sequence | Table):
|
|
145
|
+
return [len(value)]
|
|
146
|
+
else:
|
|
147
|
+
raise TypeError(f"Can't make shape for {value}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def make_datakey(
|
|
151
|
+
datatype: type[SignalDatatypeT],
|
|
152
|
+
value: SignalDatatypeT,
|
|
153
|
+
source: str,
|
|
154
|
+
metadata: SignalMetadata,
|
|
155
|
+
) -> DataKey:
|
|
156
|
+
dtn = _datakey_dtype_numpy(datatype, value)
|
|
157
|
+
return DataKey(
|
|
158
|
+
dtype=_datakey_dtype(datatype),
|
|
159
|
+
shape=_datakey_shape(value),
|
|
160
|
+
# Ignore until https://github.com/bluesky/event-model/issues/308
|
|
161
|
+
dtype_numpy=dtn.descr if len(dtn.descr) > 1 else dtn.str, # type: ignore
|
|
162
|
+
source=source,
|
|
163
|
+
**metadata,
|
|
164
|
+
)
|
|
@@ -1,244 +1,175 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import inspect
|
|
4
3
|
import time
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Generic, get_origin
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
from bluesky.protocols import Reading
|
|
11
11
|
from event_model import DataKey
|
|
12
|
-
from event_model.documents.event_descriptor import Dtype
|
|
13
|
-
from pydantic import BaseModel
|
|
14
|
-
from typing_extensions import TypedDict
|
|
15
12
|
|
|
16
13
|
from ._signal_backend import (
|
|
17
|
-
|
|
14
|
+
Array1D,
|
|
15
|
+
EnumT,
|
|
16
|
+
Primitive,
|
|
17
|
+
PrimitiveT,
|
|
18
18
|
SignalBackend,
|
|
19
|
+
SignalDatatype,
|
|
20
|
+
SignalDatatypeT,
|
|
21
|
+
SignalMetadata,
|
|
22
|
+
TableT,
|
|
23
|
+
make_datakey,
|
|
19
24
|
)
|
|
20
|
-
from .
|
|
21
|
-
|
|
22
|
-
ReadingValueCallback,
|
|
23
|
-
T,
|
|
24
|
-
get_dtype,
|
|
25
|
-
is_pydantic_model,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
primitive_dtypes: dict[type, Dtype] = {
|
|
29
|
-
str: "string",
|
|
30
|
-
int: "integer",
|
|
31
|
-
float: "number",
|
|
32
|
-
bool: "boolean",
|
|
33
|
-
}
|
|
25
|
+
from ._table import Table
|
|
26
|
+
from ._utils import Callback, get_dtype, get_enum_cls
|
|
34
27
|
|
|
35
28
|
|
|
36
|
-
class
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
class SoftConverter(Generic[SignalDatatypeT]):
|
|
30
|
+
# This is Any -> SignalDatatypeT because we support coercing
|
|
31
|
+
# value types to SignalDatatype to allow people to do things like
|
|
32
|
+
# SignalRW[Enum].set("enum value")
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def write_value(self, value: Any) -> SignalDatatypeT: ...
|
|
39
35
|
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
@dataclass
|
|
38
|
+
class PrimitiveSoftConverter(SoftConverter[PrimitiveT]):
|
|
39
|
+
datatype: type[PrimitiveT]
|
|
44
40
|
|
|
45
|
-
def write_value(self, value:
|
|
46
|
-
return value
|
|
41
|
+
def write_value(self, value: Any) -> PrimitiveT:
|
|
42
|
+
return self.datatype(value) if value else self.datatype()
|
|
47
43
|
|
|
48
|
-
def reading(self, value: T, timestamp: float, severity: int) -> Reading:
|
|
49
|
-
return Reading(
|
|
50
|
-
value=value,
|
|
51
|
-
timestamp=timestamp,
|
|
52
|
-
alarm_severity=-1 if severity > 2 else severity,
|
|
53
|
-
)
|
|
54
44
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if np.issubdtype(dtype, np.integer):
|
|
59
|
-
dtype = int
|
|
60
|
-
elif np.issubdtype(dtype, np.floating):
|
|
61
|
-
dtype = float
|
|
62
|
-
assert (
|
|
63
|
-
dtype in primitive_dtypes
|
|
64
|
-
), f"invalid converter for value of type {type(value)}"
|
|
65
|
-
dk["dtype"] = primitive_dtypes[dtype]
|
|
66
|
-
# type ignore until https://github.com/bluesky/event-model/issues/308
|
|
67
|
-
try:
|
|
68
|
-
dk["dtype_numpy"] = np.dtype(dtype).descr[0][1] # type: ignore
|
|
69
|
-
except TypeError:
|
|
70
|
-
dk["dtype_numpy"] = "" # type: ignore
|
|
71
|
-
return dk
|
|
72
|
-
|
|
73
|
-
def make_initial_value(self, datatype: type[T] | None) -> T:
|
|
74
|
-
if datatype is None:
|
|
75
|
-
return cast(T, None)
|
|
76
|
-
|
|
77
|
-
return datatype()
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
class SoftArrayConverter(SoftConverter):
|
|
81
|
-
def get_datakey(self, source: str, value, **metadata) -> DataKey:
|
|
82
|
-
dtype_numpy = ""
|
|
83
|
-
if isinstance(value, list):
|
|
84
|
-
if len(value) > 0:
|
|
85
|
-
dtype_numpy = np.dtype(type(value[0])).descr[0][1]
|
|
86
|
-
else:
|
|
87
|
-
dtype_numpy = np.dtype(value.dtype).descr[0][1]
|
|
45
|
+
class SequenceStrSoftConverter(SoftConverter[Sequence[str]]):
|
|
46
|
+
def write_value(self, value: Any) -> Sequence[str]:
|
|
47
|
+
return [str(v) for v in value] if value else []
|
|
88
48
|
|
|
89
|
-
return {
|
|
90
|
-
"source": source,
|
|
91
|
-
"dtype": "array",
|
|
92
|
-
"dtype_numpy": dtype_numpy, # type: ignore
|
|
93
|
-
"shape": [len(value)],
|
|
94
|
-
**metadata,
|
|
95
|
-
}
|
|
96
49
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
50
|
+
@dataclass
|
|
51
|
+
class SequenceEnumSoftConverter(SoftConverter[Sequence[EnumT]]):
|
|
52
|
+
datatype: type[EnumT]
|
|
100
53
|
|
|
101
|
-
|
|
102
|
-
|
|
54
|
+
def write_value(self, value: Any) -> Sequence[EnumT]:
|
|
55
|
+
return [self.datatype(v) for v in value] if value else []
|
|
103
56
|
|
|
104
|
-
return cast(T, datatype(shape=0)) # type: ignore
|
|
105
57
|
|
|
58
|
+
@dataclass
|
|
59
|
+
class NDArraySoftConverter(SoftConverter[Array1D]):
|
|
60
|
+
datatype: np.dtype
|
|
106
61
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def __init__(self, datatype: RuntimeSubsetEnum | type[Enum]):
|
|
111
|
-
if issubclass(datatype, Enum): # type: ignore
|
|
112
|
-
self.choices = tuple(v.value for v in datatype)
|
|
113
|
-
else:
|
|
114
|
-
self.choices = datatype.choices
|
|
115
|
-
|
|
116
|
-
def write_value(self, value: Enum | str) -> str:
|
|
117
|
-
return value # type: ignore
|
|
62
|
+
def write_value(self, value: Any) -> Array1D:
|
|
63
|
+
return np.array(() if value is None else value, dtype=self.datatype)
|
|
118
64
|
|
|
119
|
-
def get_datakey(self, source: str, value, **metadata) -> DataKey:
|
|
120
|
-
return {
|
|
121
|
-
"source": source,
|
|
122
|
-
"dtype": "string",
|
|
123
|
-
# type ignore until https://github.com/bluesky/event-model/issues/308
|
|
124
|
-
"dtype_numpy": "|S40", # type: ignore
|
|
125
|
-
"shape": [],
|
|
126
|
-
"choices": self.choices,
|
|
127
|
-
**metadata,
|
|
128
|
-
}
|
|
129
65
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
66
|
+
@dataclass
|
|
67
|
+
class EnumSoftConverter(SoftConverter[EnumT]):
|
|
68
|
+
datatype: type[EnumT]
|
|
133
69
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
70
|
+
def write_value(self, value: Any) -> EnumT:
|
|
71
|
+
return (
|
|
72
|
+
self.datatype(value)
|
|
73
|
+
if value
|
|
74
|
+
else list(self.datatype.__members__.values())[0]
|
|
75
|
+
)
|
|
137
76
|
|
|
138
77
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
78
|
+
@dataclass
|
|
79
|
+
class TableSoftConverter(SoftConverter[TableT]):
|
|
80
|
+
datatype: type[TableT]
|
|
142
81
|
|
|
143
|
-
def write_value(self, value):
|
|
82
|
+
def write_value(self, value: Any) -> TableT:
|
|
144
83
|
if isinstance(value, dict):
|
|
145
84
|
return self.datatype(**value)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if
|
|
157
|
-
return
|
|
158
|
-
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
85
|
+
elif isinstance(value, self.datatype):
|
|
86
|
+
return value
|
|
87
|
+
elif value is None:
|
|
88
|
+
return self.datatype()
|
|
89
|
+
else:
|
|
90
|
+
raise TypeError(f"Cannot convert {value} to {self.datatype}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def make_converter(datatype: type[SignalDatatype]) -> SoftConverter:
|
|
94
|
+
enum_cls = get_enum_cls(datatype)
|
|
95
|
+
if datatype == Sequence[str]:
|
|
96
|
+
return SequenceStrSoftConverter()
|
|
97
|
+
elif get_origin(datatype) == Sequence and enum_cls:
|
|
98
|
+
return SequenceEnumSoftConverter(enum_cls)
|
|
99
|
+
elif get_origin(datatype) == np.ndarray:
|
|
100
|
+
return NDArraySoftConverter(get_dtype(datatype))
|
|
101
|
+
elif enum_cls:
|
|
102
|
+
return EnumSoftConverter(enum_cls)
|
|
103
|
+
elif issubclass(datatype, Table):
|
|
104
|
+
return TableSoftConverter(datatype)
|
|
105
|
+
elif issubclass(datatype, Primitive):
|
|
106
|
+
return PrimitiveSoftConverter(datatype)
|
|
107
|
+
raise TypeError(f"Can't make converter for {datatype}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class SoftSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
167
111
|
"""An backend to a soft Signal, for test signals see ``MockSignalBackend``."""
|
|
168
112
|
|
|
169
|
-
_value: T
|
|
170
|
-
_initial_value: T | None
|
|
171
|
-
_timestamp: float
|
|
172
|
-
_severity: int
|
|
173
|
-
|
|
174
|
-
@classmethod
|
|
175
|
-
def datatype_allowed(cls, dtype: type) -> bool:
|
|
176
|
-
return True # Any value allowed in a soft signal
|
|
177
|
-
|
|
178
113
|
def __init__(
|
|
179
114
|
self,
|
|
180
|
-
datatype: type[
|
|
181
|
-
initial_value:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
self.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
115
|
+
datatype: type[SignalDatatypeT] | None,
|
|
116
|
+
initial_value: SignalDatatypeT | None = None,
|
|
117
|
+
units: str | None = None,
|
|
118
|
+
precision: int | None = None,
|
|
119
|
+
):
|
|
120
|
+
# Create the right converter for the datatype
|
|
121
|
+
self.converter = make_converter(datatype or float)
|
|
122
|
+
# Add the extra static metadata to the dictionary
|
|
123
|
+
self.metadata: SignalMetadata = {}
|
|
124
|
+
if units is not None:
|
|
125
|
+
self.metadata["units"] = units
|
|
126
|
+
if precision is not None:
|
|
127
|
+
self.metadata["precision"] = precision
|
|
128
|
+
if enum_cls := get_enum_cls(datatype):
|
|
129
|
+
self.metadata["choices"] = [v.value for v in enum_cls]
|
|
130
|
+
# Create and set the initial value
|
|
131
|
+
self.initial_value = self.converter.write_value(initial_value)
|
|
132
|
+
self.reading: Reading[SignalDatatypeT]
|
|
133
|
+
self.callback: Callback[Reading[SignalDatatypeT]] | None = None
|
|
134
|
+
self.set_value(self.initial_value)
|
|
135
|
+
super().__init__(datatype)
|
|
136
|
+
|
|
137
|
+
def set_value(self, value: SignalDatatypeT):
|
|
138
|
+
self.reading = Reading(
|
|
139
|
+
value=self.converter.write_value(value),
|
|
140
|
+
timestamp=time.monotonic(),
|
|
141
|
+
alarm_severity=0,
|
|
142
|
+
)
|
|
143
|
+
if self.callback:
|
|
144
|
+
self.callback(self.reading)
|
|
196
145
|
|
|
197
|
-
def source(self, name: str) -> str:
|
|
146
|
+
def source(self, name: str, read: bool) -> str:
|
|
198
147
|
return f"soft://{name}"
|
|
199
148
|
|
|
200
|
-
async def connect(self, timeout: float
|
|
201
|
-
"""Connection isn't required for soft signals."""
|
|
149
|
+
async def connect(self, timeout: float):
|
|
202
150
|
pass
|
|
203
151
|
|
|
204
|
-
async def put(self, value:
|
|
205
|
-
write_value =
|
|
206
|
-
|
|
207
|
-
if value is not None
|
|
208
|
-
else self._initial_value
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
self.set_value(write_value) # type: ignore
|
|
212
|
-
|
|
213
|
-
def set_value(self, value: T):
|
|
214
|
-
"""Method to bypass asynchronous logic."""
|
|
215
|
-
self._value = value
|
|
216
|
-
self._timestamp = time.monotonic()
|
|
217
|
-
reading: Reading = self.converter.reading(
|
|
218
|
-
self._value, self._timestamp, self._severity
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
if self.callback:
|
|
222
|
-
self.callback(reading, self._value)
|
|
152
|
+
async def put(self, value: SignalDatatypeT | None, wait: bool) -> None:
|
|
153
|
+
write_value = self.initial_value if value is None else value
|
|
154
|
+
self.set_value(write_value)
|
|
223
155
|
|
|
224
156
|
async def get_datakey(self, source: str) -> DataKey:
|
|
225
|
-
return
|
|
157
|
+
return make_datakey(
|
|
158
|
+
self.datatype or float, self.reading["value"], source, self.metadata
|
|
159
|
+
)
|
|
226
160
|
|
|
227
|
-
async def get_reading(self) -> Reading:
|
|
228
|
-
return self.
|
|
161
|
+
async def get_reading(self) -> Reading[SignalDatatypeT]:
|
|
162
|
+
return self.reading
|
|
229
163
|
|
|
230
|
-
async def get_value(self) ->
|
|
231
|
-
return self.
|
|
164
|
+
async def get_value(self) -> SignalDatatypeT:
|
|
165
|
+
return self.reading["value"]
|
|
232
166
|
|
|
233
|
-
async def get_setpoint(self) ->
|
|
234
|
-
|
|
235
|
-
return
|
|
167
|
+
async def get_setpoint(self) -> SignalDatatypeT:
|
|
168
|
+
# For a soft signal, the setpoint and readback values are the same.
|
|
169
|
+
return self.reading["value"]
|
|
236
170
|
|
|
237
|
-
def set_callback(self, callback:
|
|
171
|
+
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
238
172
|
if callback:
|
|
239
173
|
assert not self.callback, "Cannot set a callback when one is already set"
|
|
240
|
-
|
|
241
|
-
self._value, self._timestamp, self._severity
|
|
242
|
-
)
|
|
243
|
-
callback(reading, self._value)
|
|
174
|
+
callback(self.reading)
|
|
244
175
|
self.callback = callback
|
ophyd_async/core/_status.py
CHANGED
|
@@ -13,6 +13,7 @@ from typing import (
|
|
|
13
13
|
|
|
14
14
|
from bluesky.protocols import Status
|
|
15
15
|
|
|
16
|
+
from ._device import Device
|
|
16
17
|
from ._protocol import Watcher
|
|
17
18
|
from ._utils import Callback, P, T, WatcherUpdate
|
|
18
19
|
|
|
@@ -23,13 +24,14 @@ WAS = TypeVar("WAS", bound="WatchableAsyncStatus")
|
|
|
23
24
|
class AsyncStatusBase(Status):
|
|
24
25
|
"""Convert asyncio awaitable to bluesky Status interface"""
|
|
25
26
|
|
|
26
|
-
def __init__(self, awaitable: Coroutine | asyncio.Task):
|
|
27
|
+
def __init__(self, awaitable: Coroutine | asyncio.Task, name: str | None = None):
|
|
27
28
|
if isinstance(awaitable, asyncio.Task):
|
|
28
29
|
self.task = awaitable
|
|
29
30
|
else:
|
|
30
31
|
self.task = asyncio.create_task(awaitable)
|
|
31
32
|
self.task.add_done_callback(self._run_callbacks)
|
|
32
33
|
self._callbacks: list[Callback[Status]] = []
|
|
34
|
+
self._name = name
|
|
33
35
|
|
|
34
36
|
def __await__(self):
|
|
35
37
|
return self.task.__await__()
|
|
@@ -76,7 +78,11 @@ class AsyncStatusBase(Status):
|
|
|
76
78
|
status = "done"
|
|
77
79
|
else:
|
|
78
80
|
status = "pending"
|
|
79
|
-
|
|
81
|
+
device_str = f"device: {self._name}, " if self._name else ""
|
|
82
|
+
return (
|
|
83
|
+
f"<{type(self).__name__}, {device_str}"
|
|
84
|
+
f"task: {self.task.get_coro()}, {status}>"
|
|
85
|
+
)
|
|
80
86
|
|
|
81
87
|
__str__ = __repr__
|
|
82
88
|
|
|
@@ -90,7 +96,11 @@ class AsyncStatus(AsyncStatusBase):
|
|
|
90
96
|
|
|
91
97
|
@functools.wraps(f)
|
|
92
98
|
def wrap_f(*args: P.args, **kwargs: P.kwargs) -> AS:
|
|
93
|
-
|
|
99
|
+
if args and isinstance(args[0], Device):
|
|
100
|
+
name = args[0].name
|
|
101
|
+
else:
|
|
102
|
+
name = None
|
|
103
|
+
return cls(f(*args, **kwargs), name=name)
|
|
94
104
|
|
|
95
105
|
# type is actually functools._Wrapped[P, Awaitable, P, AS]
|
|
96
106
|
# but functools._Wrapped is not necessarily available
|
|
@@ -100,11 +110,13 @@ class AsyncStatus(AsyncStatusBase):
|
|
|
100
110
|
class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
|
|
101
111
|
"""Convert AsyncIterator of WatcherUpdates to bluesky Status interface."""
|
|
102
112
|
|
|
103
|
-
def __init__(
|
|
113
|
+
def __init__(
|
|
114
|
+
self, iterator: AsyncIterator[WatcherUpdate[T]], name: str | None = None
|
|
115
|
+
):
|
|
104
116
|
self._watchers: list[Watcher] = []
|
|
105
117
|
self._start = time.monotonic()
|
|
106
118
|
self._last_update: WatcherUpdate[T] | None = None
|
|
107
|
-
super().__init__(self._notify_watchers_from(iterator))
|
|
119
|
+
super().__init__(self._notify_watchers_from(iterator), name)
|
|
108
120
|
|
|
109
121
|
async def _notify_watchers_from(self, iterator: AsyncIterator[WatcherUpdate[T]]):
|
|
110
122
|
async for update in iterator:
|
|
@@ -136,7 +148,11 @@ class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
|
|
|
136
148
|
|
|
137
149
|
@functools.wraps(f)
|
|
138
150
|
def wrap_f(*args: P.args, **kwargs: P.kwargs) -> WAS:
|
|
139
|
-
|
|
151
|
+
if args and isinstance(args[0], Device):
|
|
152
|
+
name = args[0].name
|
|
153
|
+
else:
|
|
154
|
+
name = None
|
|
155
|
+
return cls(f(*args, **kwargs), name=name)
|
|
140
156
|
|
|
141
157
|
return cast(Callable[P, WAS], wrap_f)
|
|
142
158
|
|