ophyd-async 0.7.0a1__py3-none-any.whl → 0.8.0a3__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 +30 -9
- ophyd_async/core/_detector.py +5 -10
- ophyd_async/core/_device.py +146 -67
- ophyd_async/core/_device_filler.py +269 -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 +133 -134
- 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 +79 -18
- ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
- ophyd_async/epics/adaravis/_aravis_io.py +8 -6
- ophyd_async/epics/adcore/_core_io.py +5 -7
- ophyd_async/epics/adcore/_hdf_writer.py +2 -2
- ophyd_async/epics/adcore/_single_trigger.py +4 -9
- 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 +4 -5
- ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus_io.py +3 -4
- 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 +8 -9
- ophyd_async/epics/core/__init__.py +26 -0
- ophyd_async/epics/core/_aioca.py +323 -0
- ophyd_async/epics/core/_epics_connector.py +53 -0
- ophyd_async/epics/core/_epics_device.py +13 -0
- ophyd_async/epics/core/_p4p.py +382 -0
- ophyd_async/epics/core/_pvi_connector.py +92 -0
- ophyd_async/epics/core/_signal.py +171 -0
- ophyd_async/epics/core/_util.py +61 -0
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/_sensor.py +14 -13
- ophyd_async/epics/eiger/_eiger.py +1 -2
- ophyd_async/epics/eiger/_eiger_controller.py +1 -1
- ophyd_async/epics/eiger/_eiger_io.py +3 -5
- ophyd_async/epics/eiger/_odin_io.py +5 -5
- ophyd_async/epics/motor.py +4 -5
- ophyd_async/epics/signal.py +11 -0
- ophyd_async/fastcs/core.py +9 -0
- ophyd_async/fastcs/panda/__init__.py +4 -4
- ophyd_async/fastcs/panda/_block.py +23 -11
- 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 +4 -7
- 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/sim/demo/_sim_motor.py +3 -4
- ophyd_async/tango/__init__.py +2 -4
- ophyd_async/tango/base_devices/_base_device.py +76 -144
- ophyd_async/tango/demo/_counter.py +8 -18
- ophyd_async/tango/demo/_mover.py +5 -6
- 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.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/METADATA +8 -12
- ophyd_async-0.8.0a3.dist-info/RECORD +112 -0
- {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/WHEEL +1 -1
- ophyd_async/epics/pvi/__init__.py +0 -3
- ophyd_async/epics/pvi/_pvi.py +0 -338
- ophyd_async/epics/signal/__init__.py +0 -21
- ophyd_async/epics/signal/_aioca.py +0 -378
- ophyd_async/epics/signal/_common.py +0 -57
- ophyd_async/epics/signal/_epics_transport.py +0 -34
- ophyd_async/epics/signal/_p4p.py +0 -518
- ophyd_async/epics/signal/_signal.py +0 -114
- ophyd_async-0.7.0a1.dist-info/RECORD +0 -108
- {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/LICENSE +0 -0
- {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
AsyncStatus,
|
|
5
|
+
DetectorController,
|
|
6
|
+
DetectorTrigger,
|
|
7
|
+
TriggerInfo,
|
|
8
|
+
)
|
|
6
9
|
from ophyd_async.epics import adcore
|
|
7
10
|
|
|
8
11
|
from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from ophyd_async.core import StrictEnum
|
|
3
2
|
from ophyd_async.epics import adcore
|
|
4
|
-
from ophyd_async.epics.
|
|
3
|
+
from ophyd_async.epics.core import epics_signal_rw_rbv
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
class VimbaPixelFormat(
|
|
6
|
+
class VimbaPixelFormat(StrictEnum):
|
|
8
7
|
internal = "Mono8"
|
|
9
8
|
ext_enable = "Mono12"
|
|
10
9
|
ext_trigger = "Ext. Trigger"
|
|
@@ -12,7 +11,7 @@ class VimbaPixelFormat(str, Enum):
|
|
|
12
11
|
alignment = "Alignment"
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
class VimbaConvertFormat(
|
|
14
|
+
class VimbaConvertFormat(StrictEnum):
|
|
16
15
|
none = "None"
|
|
17
16
|
mono8 = "Mono8"
|
|
18
17
|
mono16 = "Mono16"
|
|
@@ -20,7 +19,7 @@ class VimbaConvertFormat(str, Enum):
|
|
|
20
19
|
rgb16 = "RGB16"
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
class VimbaTriggerSource(
|
|
22
|
+
class VimbaTriggerSource(StrictEnum):
|
|
24
23
|
freerun = "Freerun"
|
|
25
24
|
line1 = "Line1"
|
|
26
25
|
line2 = "Line2"
|
|
@@ -30,17 +29,17 @@ class VimbaTriggerSource(str, Enum):
|
|
|
30
29
|
action1 = "Action1"
|
|
31
30
|
|
|
32
31
|
|
|
33
|
-
class VimbaOverlap(
|
|
32
|
+
class VimbaOverlap(StrictEnum):
|
|
34
33
|
off = "Off"
|
|
35
34
|
prev_frame = "PreviousFrame"
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
class VimbaOnOff(
|
|
37
|
+
class VimbaOnOff(StrictEnum):
|
|
39
38
|
on = "On"
|
|
40
39
|
off = "Off"
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
class VimbaExposeOutMode(
|
|
42
|
+
class VimbaExposeOutMode(StrictEnum):
|
|
44
43
|
timed = "Timed" # Use ExposureTime PV
|
|
45
44
|
trigger_width = "TriggerWidth" # Expose for length of high signal
|
|
46
45
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from ._epics_connector import EpicsDeviceConnector, PvSuffix
|
|
2
|
+
from ._epics_device import EpicsDevice
|
|
3
|
+
from ._pvi_connector import PviDeviceConnector
|
|
4
|
+
from ._signal import (
|
|
5
|
+
CaSignalBackend,
|
|
6
|
+
PvaSignalBackend,
|
|
7
|
+
epics_signal_r,
|
|
8
|
+
epics_signal_rw,
|
|
9
|
+
epics_signal_rw_rbv,
|
|
10
|
+
epics_signal_w,
|
|
11
|
+
epics_signal_x,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"PviDeviceConnector",
|
|
16
|
+
"EpicsDeviceConnector",
|
|
17
|
+
"PvSuffix",
|
|
18
|
+
"EpicsDevice",
|
|
19
|
+
"CaSignalBackend",
|
|
20
|
+
"PvaSignalBackend",
|
|
21
|
+
"epics_signal_r",
|
|
22
|
+
"epics_signal_rw",
|
|
23
|
+
"epics_signal_rw_rbv",
|
|
24
|
+
"epics_signal_w",
|
|
25
|
+
"epics_signal_x",
|
|
26
|
+
]
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from math import isnan, nan
|
|
5
|
+
from typing import Any, Generic, cast
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from aioca import (
|
|
9
|
+
FORMAT_CTRL,
|
|
10
|
+
FORMAT_RAW,
|
|
11
|
+
FORMAT_TIME,
|
|
12
|
+
CANothing,
|
|
13
|
+
Subscription,
|
|
14
|
+
caget,
|
|
15
|
+
camonitor,
|
|
16
|
+
caput,
|
|
17
|
+
)
|
|
18
|
+
from aioca.types import AugmentedValue, Dbr, Format
|
|
19
|
+
from bluesky.protocols import Reading
|
|
20
|
+
from epicscorelibs.ca import dbr
|
|
21
|
+
from event_model import DataKey, Limits, LimitsRange
|
|
22
|
+
|
|
23
|
+
from ophyd_async.core import (
|
|
24
|
+
Array1D,
|
|
25
|
+
Callback,
|
|
26
|
+
NotConnected,
|
|
27
|
+
SignalDatatype,
|
|
28
|
+
SignalDatatypeT,
|
|
29
|
+
SignalMetadata,
|
|
30
|
+
get_enum_cls,
|
|
31
|
+
get_unique,
|
|
32
|
+
make_datakey,
|
|
33
|
+
wait_for_connection,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from ._util import EpicsSignalBackend, format_datatype, get_supported_values
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
40
|
+
def get_limits(limit: str) -> LimitsRange | None:
|
|
41
|
+
low = getattr(value, f"lower_{limit}_limit", nan)
|
|
42
|
+
high = getattr(value, f"upper_{limit}_limit", nan)
|
|
43
|
+
if not (isnan(low) and isnan(high)):
|
|
44
|
+
return LimitsRange(
|
|
45
|
+
low=None if isnan(low) else low,
|
|
46
|
+
high=None if isnan(high) else high,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
limits = Limits()
|
|
50
|
+
if limits_range := get_limits("alarm"):
|
|
51
|
+
limits["alarm"] = limits_range
|
|
52
|
+
if limits_range := get_limits("ctrl"):
|
|
53
|
+
limits["control"] = limits_range
|
|
54
|
+
if limits_range := get_limits("disp"):
|
|
55
|
+
limits["display"] = limits_range
|
|
56
|
+
if limits_range := get_limits("warning"):
|
|
57
|
+
limits["warning"] = limits_range
|
|
58
|
+
return limits
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _metadata_from_augmented_value(
|
|
62
|
+
value: AugmentedValue, metadata: SignalMetadata
|
|
63
|
+
) -> SignalMetadata:
|
|
64
|
+
metadata = metadata.copy()
|
|
65
|
+
if hasattr(value, "units"):
|
|
66
|
+
metadata["units"] = value.units
|
|
67
|
+
if hasattr(value, "precision") and not isnan(value.precision):
|
|
68
|
+
metadata["precision"] = value.precision
|
|
69
|
+
if limits := _limits_from_augmented_value(value):
|
|
70
|
+
metadata["limits"] = limits
|
|
71
|
+
return metadata
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CaConverter(Generic[SignalDatatypeT]):
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
datatype: type[SignalDatatypeT],
|
|
78
|
+
read_dbr: Dbr,
|
|
79
|
+
write_dbr: Dbr | None = None,
|
|
80
|
+
metadata: SignalMetadata | None = None,
|
|
81
|
+
):
|
|
82
|
+
self.datatype = datatype
|
|
83
|
+
self.read_dbr: Dbr = read_dbr
|
|
84
|
+
self.write_dbr: Dbr | None = write_dbr
|
|
85
|
+
self.metadata = metadata or SignalMetadata()
|
|
86
|
+
|
|
87
|
+
def write_value(self, value: Any) -> Any:
|
|
88
|
+
# The ca library will do the conversion for us
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
def value(self, value: AugmentedValue) -> SignalDatatypeT:
|
|
92
|
+
# for channel access ca_xxx classes, this
|
|
93
|
+
# invokes __pos__ operator to return an instance of
|
|
94
|
+
# the builtin base class
|
|
95
|
+
return +value # type: ignore
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DisconnectedCaConverter(CaConverter):
|
|
99
|
+
def __getattribute__(self, __name: str) -> Any:
|
|
100
|
+
raise NotImplementedError("No PV has been set as connect() has not been called")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class CaArrayConverter(CaConverter[np.ndarray]):
|
|
104
|
+
def value(self, value: AugmentedValue) -> np.ndarray:
|
|
105
|
+
# A less expensive conversion
|
|
106
|
+
return np.array(value, copy=False)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CaSequenceStrConverter(CaConverter[Sequence[str]]):
|
|
110
|
+
def value(self, value: AugmentedValue) -> Sequence[str]:
|
|
111
|
+
return [str(v) for v in value] # type: ignore
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class CaLongStrConverter(CaConverter[str]):
|
|
115
|
+
def __init__(self):
|
|
116
|
+
super().__init__(str, dbr.DBR_CHAR_STR, dbr.DBR_CHAR_STR)
|
|
117
|
+
|
|
118
|
+
def write_value_and_dbr(self, value: Any) -> Any:
|
|
119
|
+
# Add a null in here as this is what the commandline caput does
|
|
120
|
+
# TODO: this should be in the server so check if it can be pushed to asyn
|
|
121
|
+
return value + "\0"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class CaBoolConverter(CaConverter[bool]):
|
|
125
|
+
def __init__(self):
|
|
126
|
+
super().__init__(bool, dbr.DBR_SHORT)
|
|
127
|
+
|
|
128
|
+
def value(self, value: AugmentedValue) -> bool:
|
|
129
|
+
return bool(value)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class CaEnumConverter(CaConverter[str]):
|
|
133
|
+
def __init__(self, supported_values: dict[str, str]):
|
|
134
|
+
self.supported_values = supported_values
|
|
135
|
+
super().__init__(
|
|
136
|
+
str, dbr.DBR_STRING, metadata=SignalMetadata(choices=list(supported_values))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def value(self, value: AugmentedValue) -> str:
|
|
140
|
+
return self.supported_values[str(value)]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_datatype_converter_from_dbr: dict[
|
|
144
|
+
tuple[Dbr, bool], tuple[type[SignalDatatype], type[CaConverter]]
|
|
145
|
+
] = {
|
|
146
|
+
(dbr.DBR_STRING, False): (str, CaConverter),
|
|
147
|
+
(dbr.DBR_SHORT, False): (int, CaConverter),
|
|
148
|
+
(dbr.DBR_FLOAT, False): (float, CaConverter),
|
|
149
|
+
(dbr.DBR_ENUM, False): (str, CaConverter),
|
|
150
|
+
(dbr.DBR_CHAR, False): (int, CaConverter),
|
|
151
|
+
(dbr.DBR_LONG, False): (int, CaConverter),
|
|
152
|
+
(dbr.DBR_DOUBLE, False): (float, CaConverter),
|
|
153
|
+
(dbr.DBR_STRING, True): (Sequence[str], CaSequenceStrConverter),
|
|
154
|
+
(dbr.DBR_SHORT, True): (Array1D[np.int16], CaArrayConverter),
|
|
155
|
+
(dbr.DBR_FLOAT, True): (Array1D[np.float32], CaArrayConverter),
|
|
156
|
+
(dbr.DBR_ENUM, True): (Sequence[str], CaSequenceStrConverter),
|
|
157
|
+
(dbr.DBR_CHAR, True): (Array1D[np.uint8], CaArrayConverter),
|
|
158
|
+
(dbr.DBR_LONG, True): (Array1D[np.int32], CaArrayConverter),
|
|
159
|
+
(dbr.DBR_DOUBLE, True): (Array1D[np.float64], CaArrayConverter),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def make_converter(
|
|
164
|
+
datatype: type | None, values: dict[str, AugmentedValue]
|
|
165
|
+
) -> CaConverter:
|
|
166
|
+
pv = list(values)[0]
|
|
167
|
+
pv_dbr = cast(
|
|
168
|
+
Dbr, get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
|
|
169
|
+
)
|
|
170
|
+
is_array = bool([v for v in values.values() if v.element_count > 1])
|
|
171
|
+
# Infer a datatype and converter from the dbr
|
|
172
|
+
inferred_datatype, converter_cls = _datatype_converter_from_dbr[(pv_dbr, is_array)]
|
|
173
|
+
# Some override cases
|
|
174
|
+
if is_array and pv_dbr == dbr.DBR_CHAR and datatype is str:
|
|
175
|
+
# Override waveform of chars to be treated as string
|
|
176
|
+
return CaLongStrConverter()
|
|
177
|
+
elif not is_array and datatype is bool and pv_dbr == dbr.DBR_ENUM:
|
|
178
|
+
# Database can't do bools, so are often representated as enums of len 2
|
|
179
|
+
pv_num_choices = get_unique(
|
|
180
|
+
{k: len(v.enums) for k, v in values.items()}, "number of choices"
|
|
181
|
+
)
|
|
182
|
+
if pv_num_choices != 2:
|
|
183
|
+
raise TypeError(f"{pv} has {pv_num_choices} choices, can't map to bool")
|
|
184
|
+
return CaBoolConverter()
|
|
185
|
+
elif not is_array and pv_dbr == dbr.DBR_ENUM:
|
|
186
|
+
pv_choices = get_unique(
|
|
187
|
+
{k: tuple(v.enums) for k, v in values.items()}, "choices"
|
|
188
|
+
)
|
|
189
|
+
if enum_cls := get_enum_cls(datatype):
|
|
190
|
+
# If explicitly requested then check
|
|
191
|
+
return CaEnumConverter(get_supported_values(pv, enum_cls, pv_choices))
|
|
192
|
+
elif datatype in (None, str):
|
|
193
|
+
# Drop to string for safety, but retain choices as metadata
|
|
194
|
+
return CaConverter(
|
|
195
|
+
str,
|
|
196
|
+
dbr.DBR_STRING,
|
|
197
|
+
metadata=SignalMetadata(choices=list(pv_choices)),
|
|
198
|
+
)
|
|
199
|
+
elif (
|
|
200
|
+
inferred_datatype is float
|
|
201
|
+
and datatype is int
|
|
202
|
+
and get_unique({k: v.precision for k, v in values.items()}, "precision") == 0
|
|
203
|
+
):
|
|
204
|
+
# Allow int signals to represent float records when prec is 0
|
|
205
|
+
return CaConverter(int, pv_dbr)
|
|
206
|
+
elif datatype in (None, inferred_datatype):
|
|
207
|
+
# If datatype matches what we are given then allow it and use inferred converter
|
|
208
|
+
return converter_cls(inferred_datatype, pv_dbr)
|
|
209
|
+
if pv_dbr == dbr.DBR_ENUM:
|
|
210
|
+
inferred_datatype = "str | SubsetEnum | StrictEnum"
|
|
211
|
+
raise TypeError(
|
|
212
|
+
f"{pv} with inferred datatype {format_datatype(inferred_datatype)}"
|
|
213
|
+
f" cannot be coerced to {format_datatype(datatype)}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
_tried_pyepics = False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _use_pyepics_context_if_imported():
|
|
221
|
+
global _tried_pyepics
|
|
222
|
+
if not _tried_pyepics:
|
|
223
|
+
ca = sys.modules.get("epics.ca", None)
|
|
224
|
+
if ca:
|
|
225
|
+
ca.use_initial_context()
|
|
226
|
+
_tried_pyepics = True
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
230
|
+
def __init__(
|
|
231
|
+
self,
|
|
232
|
+
datatype: type[SignalDatatypeT] | None,
|
|
233
|
+
read_pv: str = "",
|
|
234
|
+
write_pv: str = "",
|
|
235
|
+
):
|
|
236
|
+
self.converter: CaConverter = DisconnectedCaConverter(float, dbr.DBR_DOUBLE)
|
|
237
|
+
self.initial_values: dict[str, AugmentedValue] = {}
|
|
238
|
+
self.subscription: Subscription | None = None
|
|
239
|
+
super().__init__(datatype, read_pv, write_pv)
|
|
240
|
+
|
|
241
|
+
def source(self, name: str, read: bool):
|
|
242
|
+
return f"ca://{self.read_pv if read else self.write_pv}"
|
|
243
|
+
|
|
244
|
+
async def _store_initial_value(self, pv: str, timeout: float):
|
|
245
|
+
try:
|
|
246
|
+
self.initial_values[pv] = await caget(
|
|
247
|
+
pv, format=FORMAT_CTRL, timeout=timeout
|
|
248
|
+
)
|
|
249
|
+
except CANothing as exc:
|
|
250
|
+
logging.debug(f"signal ca://{pv} timed out")
|
|
251
|
+
raise NotConnected(f"ca://{pv}") from exc
|
|
252
|
+
|
|
253
|
+
async def connect(self, timeout: float):
|
|
254
|
+
_use_pyepics_context_if_imported()
|
|
255
|
+
if self.read_pv != self.write_pv:
|
|
256
|
+
# Different, need to connect both
|
|
257
|
+
await wait_for_connection(
|
|
258
|
+
read_pv=self._store_initial_value(self.read_pv, timeout=timeout),
|
|
259
|
+
write_pv=self._store_initial_value(self.write_pv, timeout=timeout),
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
# The same, so only need to connect one
|
|
263
|
+
await self._store_initial_value(self.read_pv, timeout=timeout)
|
|
264
|
+
self.converter = make_converter(self.datatype, self.initial_values)
|
|
265
|
+
|
|
266
|
+
async def _caget(self, pv: str, format: Format) -> AugmentedValue:
|
|
267
|
+
return await caget(
|
|
268
|
+
pv, datatype=self.converter.read_dbr, format=format, timeout=None
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _make_reading(self, value: AugmentedValue) -> Reading[SignalDatatypeT]:
|
|
272
|
+
return {
|
|
273
|
+
"value": self.converter.value(value),
|
|
274
|
+
"timestamp": value.timestamp,
|
|
275
|
+
"alarm_severity": -1 if value.severity > 2 else value.severity,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async def put(self, value: SignalDatatypeT | None, wait: bool):
|
|
279
|
+
if value is None:
|
|
280
|
+
write_value = self.initial_values[self.write_pv]
|
|
281
|
+
else:
|
|
282
|
+
write_value = self.converter.write_value(value)
|
|
283
|
+
await caput(
|
|
284
|
+
self.write_pv,
|
|
285
|
+
write_value,
|
|
286
|
+
datatype=self.converter.write_dbr,
|
|
287
|
+
wait=wait,
|
|
288
|
+
timeout=None,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
async def get_datakey(self, source: str) -> DataKey:
|
|
292
|
+
value = await self._caget(self.read_pv, FORMAT_CTRL)
|
|
293
|
+
metadata = _metadata_from_augmented_value(value, self.converter.metadata)
|
|
294
|
+
return make_datakey(
|
|
295
|
+
self.converter.datatype, self.converter.value(value), source, metadata
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
async def get_reading(self) -> Reading[SignalDatatypeT]:
|
|
299
|
+
value = await self._caget(self.read_pv, FORMAT_TIME)
|
|
300
|
+
return self._make_reading(value)
|
|
301
|
+
|
|
302
|
+
async def get_value(self) -> SignalDatatypeT:
|
|
303
|
+
value = await self._caget(self.read_pv, FORMAT_RAW)
|
|
304
|
+
return self.converter.value(value)
|
|
305
|
+
|
|
306
|
+
async def get_setpoint(self) -> SignalDatatypeT:
|
|
307
|
+
value = await self._caget(self.write_pv, FORMAT_RAW)
|
|
308
|
+
return self.converter.value(value)
|
|
309
|
+
|
|
310
|
+
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
311
|
+
if callback:
|
|
312
|
+
assert (
|
|
313
|
+
not self.subscription
|
|
314
|
+
), "Cannot set a callback when one is already set"
|
|
315
|
+
self.subscription = camonitor(
|
|
316
|
+
self.read_pv,
|
|
317
|
+
lambda v: callback(self._make_reading(v)),
|
|
318
|
+
datatype=self.converter.read_dbr,
|
|
319
|
+
format=FORMAT_TIME,
|
|
320
|
+
)
|
|
321
|
+
elif self.subscription:
|
|
322
|
+
self.subscription.close()
|
|
323
|
+
self.subscription = None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core import Device, DeviceConnector, DeviceFiller
|
|
7
|
+
|
|
8
|
+
from ._signal import EpicsSignalBackend, get_signal_backend_type, split_protocol_from_pv
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PvSuffix:
|
|
13
|
+
read_suffix: str
|
|
14
|
+
write_suffix: str | None = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def rbv(cls, write_suffix: str, rbv_suffix: str = "_RBV") -> PvSuffix:
|
|
18
|
+
return cls(write_suffix + rbv_suffix, write_suffix)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fill_backend_with_prefix(
|
|
22
|
+
prefix: str, backend: EpicsSignalBackend, annotations: list[Any]
|
|
23
|
+
):
|
|
24
|
+
unhandled = []
|
|
25
|
+
while annotations:
|
|
26
|
+
annotation = annotations.pop(0)
|
|
27
|
+
if isinstance(annotation, PvSuffix):
|
|
28
|
+
backend.read_pv = prefix + annotation.read_suffix
|
|
29
|
+
backend.write_pv = prefix + (
|
|
30
|
+
annotation.write_suffix or annotation.read_suffix
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
unhandled.append(annotation)
|
|
34
|
+
annotations.extend(unhandled)
|
|
35
|
+
# These leftover annotations will now be handled by the iterator
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class EpicsDeviceConnector(DeviceConnector):
|
|
39
|
+
def __init__(self, prefix: str) -> None:
|
|
40
|
+
self.prefix = prefix
|
|
41
|
+
|
|
42
|
+
def create_children_from_annotations(self, device: Device):
|
|
43
|
+
if not hasattr(self, "filler"):
|
|
44
|
+
protocol, prefix = split_protocol_from_pv(self.prefix)
|
|
45
|
+
self.filler = DeviceFiller(
|
|
46
|
+
device,
|
|
47
|
+
signal_backend_factory=get_signal_backend_type(protocol),
|
|
48
|
+
device_connector_factory=DeviceConnector,
|
|
49
|
+
)
|
|
50
|
+
for backend, annotations in self.filler.create_signals_from_annotations():
|
|
51
|
+
fill_backend_with_prefix(prefix, backend, annotations)
|
|
52
|
+
|
|
53
|
+
list(self.filler.create_devices_from_annotations())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from ophyd_async.core import Device
|
|
2
|
+
|
|
3
|
+
from ._epics_connector import EpicsDeviceConnector
|
|
4
|
+
from ._pvi_connector import PviDeviceConnector
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EpicsDevice(Device):
|
|
8
|
+
def __init__(self, prefix: str, with_pvi: bool = False, name: str = ""):
|
|
9
|
+
if with_pvi:
|
|
10
|
+
connector = PviDeviceConnector(prefix)
|
|
11
|
+
else:
|
|
12
|
+
connector = EpicsDeviceConnector(prefix)
|
|
13
|
+
super().__init__(name=name, connector=connector)
|