ophyd-async 0.8.0a5__py3-none-any.whl → 0.9.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/_version.py +2 -2
- ophyd_async/core/__init__.py +17 -46
- ophyd_async/core/_detector.py +68 -44
- ophyd_async/core/_device.py +120 -79
- ophyd_async/core/_device_filler.py +17 -8
- ophyd_async/core/_flyer.py +2 -2
- ophyd_async/core/_protocol.py +0 -28
- ophyd_async/core/_readable.py +30 -23
- ophyd_async/core/_settings.py +104 -0
- ophyd_async/core/_signal.py +164 -151
- ophyd_async/core/_signal_backend.py +4 -1
- ophyd_async/core/_soft_signal_backend.py +2 -1
- ophyd_async/core/_table.py +27 -14
- ophyd_async/core/_utils.py +30 -5
- ophyd_async/core/_yaml_settings.py +64 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +49 -0
- ophyd_async/epics/adandor/_andor_io.py +36 -0
- ophyd_async/epics/adaravis/__init__.py +3 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -37
- ophyd_async/epics/adaravis/_aravis_controller.py +21 -30
- ophyd_async/epics/adaravis/_aravis_io.py +4 -4
- ophyd_async/epics/adcore/__init__.py +15 -8
- ophyd_async/epics/adcore/_core_detector.py +41 -0
- ophyd_async/epics/adcore/_core_io.py +56 -31
- ophyd_async/epics/adcore/_core_logic.py +99 -84
- ophyd_async/epics/adcore/_core_writer.py +219 -0
- ophyd_async/epics/adcore/_hdf_writer.py +33 -59
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +5 -4
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +37 -36
- ophyd_async/epics/adkinetix/_kinetix.py +29 -24
- ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
- ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
- ophyd_async/epics/adpilatus/__init__.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
- ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
- ophyd_async/epics/adsimdetector/__init__.py +3 -3
- ophyd_async/epics/adsimdetector/_sim.py +33 -17
- ophyd_async/epics/advimba/_vimba.py +23 -23
- ophyd_async/epics/advimba/_vimba_controller.py +21 -35
- ophyd_async/epics/advimba/_vimba_io.py +23 -23
- ophyd_async/epics/core/_aioca.py +52 -21
- ophyd_async/epics/core/_p4p.py +59 -16
- ophyd_async/epics/core/_pvi_connector.py +4 -2
- ophyd_async/epics/core/_signal.py +9 -2
- ophyd_async/epics/core/_util.py +10 -1
- ophyd_async/epics/eiger/_eiger_controller.py +10 -5
- ophyd_async/epics/eiger/_eiger_io.py +3 -3
- ophyd_async/epics/motor.py +26 -15
- ophyd_async/epics/sim/_ioc.py +29 -0
- ophyd_async/epics/{demo → sim}/_mover.py +12 -6
- ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
- ophyd_async/epics/testing/__init__.py +24 -0
- ophyd_async/epics/testing/_example_ioc.py +91 -0
- ophyd_async/epics/testing/_utils.py +50 -0
- ophyd_async/epics/testing/test_records.db +174 -0
- ophyd_async/epics/testing/test_records_pva.db +177 -0
- ophyd_async/fastcs/core.py +2 -2
- ophyd_async/fastcs/panda/__init__.py +0 -2
- ophyd_async/fastcs/panda/_block.py +9 -9
- ophyd_async/fastcs/panda/_control.py +9 -4
- ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
- ophyd_async/fastcs/panda/_table.py +4 -1
- ophyd_async/fastcs/panda/_trigger.py +7 -7
- ophyd_async/plan_stubs/__init__.py +14 -0
- ophyd_async/plan_stubs/_ensure_connected.py +11 -17
- ophyd_async/plan_stubs/_fly.py +2 -2
- ophyd_async/plan_stubs/_nd_attributes.py +7 -5
- ophyd_async/plan_stubs/_panda.py +13 -0
- ophyd_async/plan_stubs/_settings.py +125 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +19 -0
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
- ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
- ophyd_async/tango/__init__.py +0 -43
- ophyd_async/tango/{signal → core}/__init__.py +7 -2
- ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
- ophyd_async/tango/{signal → core}/_signal.py +16 -4
- ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
- ophyd_async/tango/{signal → core}/_tango_transport.py +13 -15
- ophyd_async/tango/{demo → sim}/_counter.py +6 -7
- ophyd_async/tango/{demo → sim}/_mover.py +13 -9
- ophyd_async/testing/__init__.py +52 -0
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +176 -0
- ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
- ophyd_async/testing/_one_of_everything.py +126 -0
- ophyd_async/testing/_wait_for_pending.py +22 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +50 -48
- ophyd_async-0.9.0.dist-info/RECORD +129 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async/tango/base_devices/__init__.py +0 -4
- ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
- ophyd_async-0.8.0a5.dist-info/entry_points.txt +0 -2
- /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
- /ophyd_async/epics/{demo → sim}/mover.db +0 -0
- /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
- /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
ophyd_async/epics/core/_p4p.py
CHANGED
|
@@ -31,6 +31,8 @@ from ophyd_async.core import (
|
|
|
31
31
|
|
|
32
32
|
from ._util import EpicsSignalBackend, format_datatype, get_supported_values
|
|
33
33
|
|
|
34
|
+
logger = logging.getLogger("ophyd_async")
|
|
35
|
+
|
|
34
36
|
|
|
35
37
|
def _limits_from_value(value: Any) -> Limits:
|
|
36
38
|
def get_limits(
|
|
@@ -39,7 +41,7 @@ def _limits_from_value(value: Any) -> Limits:
|
|
|
39
41
|
substructure = getattr(value, substucture_name, None)
|
|
40
42
|
low = getattr(substructure, low_name, nan)
|
|
41
43
|
high = getattr(substructure, high_name, nan)
|
|
42
|
-
if not (isnan(low) and isnan(high)):
|
|
44
|
+
if not (isnan(low) and isnan(high)) and not low == high == 0:
|
|
43
45
|
return LimitsRange(
|
|
44
46
|
low=None if isnan(low) else low,
|
|
45
47
|
high=None if isnan(high) else high,
|
|
@@ -60,12 +62,22 @@ def _limits_from_value(value: Any) -> Limits:
|
|
|
60
62
|
def _metadata_from_value(datatype: type[SignalDatatype], value: Any) -> SignalMetadata:
|
|
61
63
|
metadata = SignalMetadata()
|
|
62
64
|
value_data: Any = getattr(value, "value", None)
|
|
65
|
+
specifier = _get_specifier(value)
|
|
63
66
|
display_data: Any = getattr(value, "display", None)
|
|
64
|
-
if
|
|
67
|
+
if (
|
|
68
|
+
hasattr(display_data, "units")
|
|
69
|
+
and specifier[-1] in _number_specifiers
|
|
70
|
+
and datatype is not str
|
|
71
|
+
):
|
|
65
72
|
metadata["units"] = display_data.units
|
|
66
|
-
if
|
|
73
|
+
if (
|
|
74
|
+
hasattr(display_data, "precision")
|
|
75
|
+
and not isnan(display_data.precision)
|
|
76
|
+
and specifier[-1] in _float_specifiers
|
|
77
|
+
and datatype is not int
|
|
78
|
+
):
|
|
67
79
|
metadata["precision"] = display_data.precision
|
|
68
|
-
if limits := _limits_from_value(value):
|
|
80
|
+
if (limits := _limits_from_value(value)) and specifier[-1] in _number_specifiers:
|
|
69
81
|
metadata["limits"] = limits
|
|
70
82
|
# Get choices from display or value
|
|
71
83
|
if datatype is str or issubclass(datatype, StrictEnum):
|
|
@@ -84,9 +96,7 @@ class PvaConverter(Generic[SignalDatatypeT]):
|
|
|
84
96
|
self.datatype = datatype
|
|
85
97
|
|
|
86
98
|
def value(self, value: Any) -> SignalDatatypeT:
|
|
87
|
-
#
|
|
88
|
-
# invokes __pos__ operator to return an instance of
|
|
89
|
-
# the builtin base class
|
|
99
|
+
# Normally the value will be of the correct python type
|
|
90
100
|
return value["value"]
|
|
91
101
|
|
|
92
102
|
def write_value(self, value: Any) -> Any:
|
|
@@ -94,6 +104,31 @@ class PvaConverter(Generic[SignalDatatypeT]):
|
|
|
94
104
|
return value
|
|
95
105
|
|
|
96
106
|
|
|
107
|
+
class PvaIntConverter(PvaConverter[int]):
|
|
108
|
+
def __init__(self):
|
|
109
|
+
super().__init__(int)
|
|
110
|
+
|
|
111
|
+
def value(self, value: Any) -> int:
|
|
112
|
+
# Convert to an int
|
|
113
|
+
return int(value["value"])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class PvaLongStringConverter(PvaConverter[str]):
|
|
117
|
+
def __init__(self):
|
|
118
|
+
super().__init__(str)
|
|
119
|
+
|
|
120
|
+
def value(self, value: Any) -> Any:
|
|
121
|
+
# Value here is a null terminated array of ascii codes.
|
|
122
|
+
# We strip out the null terminator, and convert each code
|
|
123
|
+
# to the corresponding char, joining into a string
|
|
124
|
+
return value["value"].tobytes().rstrip(b"\0").decode()
|
|
125
|
+
|
|
126
|
+
def write_value(self, value: Any) -> Any:
|
|
127
|
+
# Inverse of reading - convert each character into it's ascii code,
|
|
128
|
+
# put into a list, and add null terminator.
|
|
129
|
+
return np.frombuffer(str(value).encode() + b"\0", dtype=np.int8)
|
|
130
|
+
|
|
131
|
+
|
|
97
132
|
class DisconnectedPvaConverter(PvaConverter):
|
|
98
133
|
def __getattribute__(self, __name: str) -> Any:
|
|
99
134
|
raise NotImplementedError("No PV has been set as connect() has not been called")
|
|
@@ -158,6 +193,9 @@ class PvaTableConverter(PvaConverter[Table]):
|
|
|
158
193
|
|
|
159
194
|
|
|
160
195
|
# https://mdavidsaver.github.io/p4p/values.html
|
|
196
|
+
_float_specifiers = {"f", "d"}
|
|
197
|
+
_int_specifiers = {"b", "B", "h", "H", "i", "I", "l", "L"}
|
|
198
|
+
_number_specifiers = _float_specifiers.union(_int_specifiers)
|
|
161
199
|
_datatype_converter_from_typeid: dict[
|
|
162
200
|
tuple[str, str], tuple[type[SignalDatatype], type[PvaConverter]]
|
|
163
201
|
] = {
|
|
@@ -192,7 +230,7 @@ _datatype_converter_from_typeid: dict[
|
|
|
192
230
|
}
|
|
193
231
|
|
|
194
232
|
|
|
195
|
-
def _get_specifier(value: Value):
|
|
233
|
+
def _get_specifier(value: Value) -> str:
|
|
196
234
|
typ = value.type("value").aspy()
|
|
197
235
|
if isinstance(typ, tuple):
|
|
198
236
|
return typ[0]
|
|
@@ -242,7 +280,7 @@ def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverte
|
|
|
242
280
|
== 0
|
|
243
281
|
):
|
|
244
282
|
# Allow int signals to represent float records when prec is 0
|
|
245
|
-
return
|
|
283
|
+
return PvaIntConverter()
|
|
246
284
|
elif inferred_datatype is str and (enum_cls := get_enum_cls(datatype)):
|
|
247
285
|
# Allow strings to be used as enums until QSRV supports this
|
|
248
286
|
return PvaConverter(str)
|
|
@@ -252,6 +290,9 @@ def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverte
|
|
|
252
290
|
elif datatype in (None, inferred_datatype):
|
|
253
291
|
# If datatype matches what we are given then allow it and use inferred converter
|
|
254
292
|
return converter_cls(inferred_datatype)
|
|
293
|
+
# Allow waveforms with FTVL=CHAR to be treated as str when requested
|
|
294
|
+
elif datatype is str and inferred_datatype == Array1D[np.int8]:
|
|
295
|
+
return PvaLongStringConverter()
|
|
255
296
|
raise TypeError(
|
|
256
297
|
f"{pv} with inferred datatype {format_datatype(inferred_datatype)}"
|
|
257
298
|
f" from {typeid=} {specifier=}"
|
|
@@ -282,7 +323,7 @@ async def pvget_with_timeout(pv: str, timeout: float) -> Any:
|
|
|
282
323
|
try:
|
|
283
324
|
return await asyncio.wait_for(context().get(pv), timeout=timeout)
|
|
284
325
|
except asyncio.TimeoutError as exc:
|
|
285
|
-
|
|
326
|
+
logger.debug(f"signal pva://{pv} timed out", exc_info=True)
|
|
286
327
|
raise NotConnected(f"pva://{pv}") from exc
|
|
287
328
|
|
|
288
329
|
|
|
@@ -364,10 +405,15 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
364
405
|
return self.converter.value(value)
|
|
365
406
|
|
|
366
407
|
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
408
|
+
if callback and self.subscription:
|
|
409
|
+
msg = "Cannot set a callback when one is already set"
|
|
410
|
+
raise RuntimeError(msg)
|
|
411
|
+
|
|
412
|
+
if self.subscription:
|
|
413
|
+
self.subscription.close()
|
|
414
|
+
self.subscription = None
|
|
415
|
+
|
|
367
416
|
if callback:
|
|
368
|
-
assert (
|
|
369
|
-
not self.subscription
|
|
370
|
-
), "Cannot set a callback when one is already set"
|
|
371
417
|
|
|
372
418
|
async def async_callback(v):
|
|
373
419
|
callback(self._make_reading(v))
|
|
@@ -378,6 +424,3 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
378
424
|
self.subscription = context().monitor(
|
|
379
425
|
self.read_pv, async_callback, request=request
|
|
380
426
|
)
|
|
381
|
-
elif self.subscription:
|
|
382
|
-
self.subscription.close()
|
|
383
|
-
self.subscription = None
|
|
@@ -32,10 +32,11 @@ def _get_signal_details(entry: Entry) -> tuple[type[Signal], str, str]:
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
class PviDeviceConnector(DeviceConnector):
|
|
35
|
-
def __init__(self, prefix: str = "") -> None:
|
|
35
|
+
def __init__(self, prefix: str = "", error_hint: str = "") -> None:
|
|
36
36
|
# TODO: what happens if we get a leading "pva://" here?
|
|
37
37
|
self.prefix = prefix
|
|
38
38
|
self.pvi_pv = prefix + "PVI"
|
|
39
|
+
self.error_hint = error_hint
|
|
39
40
|
|
|
40
41
|
def create_children_from_annotations(self, device: Device):
|
|
41
42
|
if not hasattr(self, "filler"):
|
|
@@ -85,7 +86,8 @@ class PviDeviceConnector(DeviceConnector):
|
|
|
85
86
|
if e:
|
|
86
87
|
self._fill_child(name, e, i)
|
|
87
88
|
# Check that all the requested children have been filled
|
|
88
|
-
|
|
89
|
+
suffix = f"\n{self.error_hint}" if self.error_hint else ""
|
|
90
|
+
self.filler.check_filled(f"{self.pvi_pv}: {entries}{suffix}")
|
|
89
91
|
# Set the name of the device to name all children
|
|
90
92
|
device.set_name(device.name)
|
|
91
93
|
return await super().connect_real(device, timeout, force_reconnect)
|
|
@@ -14,7 +14,7 @@ from ophyd_async.core import (
|
|
|
14
14
|
get_unique,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
from ._util import EpicsSignalBackend
|
|
17
|
+
from ._util import EpicsSignalBackend, get_pv_basename_and_field
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class EpicsProtocol(Enum):
|
|
@@ -124,7 +124,14 @@ def epics_signal_rw_rbv(
|
|
|
124
124
|
read_suffix:
|
|
125
125
|
Append this suffix to the write pv to create the readback pv
|
|
126
126
|
"""
|
|
127
|
-
|
|
127
|
+
|
|
128
|
+
base_pv, field = get_pv_basename_and_field(write_pv)
|
|
129
|
+
if field is not None:
|
|
130
|
+
read_pv = f"{base_pv}{read_suffix}.{field}"
|
|
131
|
+
else:
|
|
132
|
+
read_pv = f"{write_pv}{read_suffix}"
|
|
133
|
+
|
|
134
|
+
return epics_signal_rw(datatype, read_pv, write_pv, name)
|
|
128
135
|
|
|
129
136
|
|
|
130
137
|
def epics_signal_r(
|
ophyd_async/epics/core/_util.py
CHANGED
|
@@ -12,6 +12,15 @@ from ophyd_async.core import (
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def get_pv_basename_and_field(pv: str) -> tuple[str, str | None]:
|
|
16
|
+
"""Simple utility function for extracting base pv name without field"""
|
|
17
|
+
|
|
18
|
+
if "." in pv:
|
|
19
|
+
return (pv.split(".", -1)[0], pv.split(".", -1)[1])
|
|
20
|
+
else:
|
|
21
|
+
return (pv, None)
|
|
22
|
+
|
|
23
|
+
|
|
15
24
|
def get_supported_values(
|
|
16
25
|
pv: str,
|
|
17
26
|
datatype: type,
|
|
@@ -38,7 +47,7 @@ def get_supported_values(
|
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
def format_datatype(datatype: Any) -> str:
|
|
41
|
-
if get_origin(datatype) is np.ndarray and get_args(datatype)
|
|
50
|
+
if get_origin(datatype) is np.ndarray and get_args(datatype):
|
|
42
51
|
dtype = get_dtype(datatype)
|
|
43
52
|
return f"Array1D[np.{dtype.name}]"
|
|
44
53
|
elif get_origin(datatype) is Sequence:
|
|
@@ -11,10 +11,10 @@ from ophyd_async.core import (
|
|
|
11
11
|
from ._eiger_io import EigerDriverIO, EigerTriggerMode
|
|
12
12
|
|
|
13
13
|
EIGER_TRIGGER_MODE_MAP = {
|
|
14
|
-
DetectorTrigger.
|
|
15
|
-
DetectorTrigger.
|
|
16
|
-
DetectorTrigger.
|
|
17
|
-
DetectorTrigger.
|
|
14
|
+
DetectorTrigger.INTERNAL: EigerTriggerMode.INTERNAL,
|
|
15
|
+
DetectorTrigger.CONSTANT_GATE: EigerTriggerMode.GATE,
|
|
16
|
+
DetectorTrigger.VARIABLE_GATE: EigerTriggerMode.GATE,
|
|
17
|
+
DetectorTrigger.EDGE_TRIGGER: EigerTriggerMode.EDGE,
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
|
|
@@ -55,7 +55,12 @@ class EigerController(DetectorController):
|
|
|
55
55
|
async def arm(self):
|
|
56
56
|
# TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
|
|
57
57
|
self._arm_status = set_and_wait_for_other_value(
|
|
58
|
-
self._drv.arm,
|
|
58
|
+
self._drv.arm,
|
|
59
|
+
1,
|
|
60
|
+
self._drv.state,
|
|
61
|
+
"ready",
|
|
62
|
+
timeout=DEFAULT_TIMEOUT,
|
|
63
|
+
wait_for_set_completion=False,
|
|
59
64
|
)
|
|
60
65
|
|
|
61
66
|
async def wait_for_idle(self):
|
|
@@ -3,9 +3,9 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw_rbv, epics_si
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class EigerTriggerMode(StrictEnum):
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
INTERNAL = "ints"
|
|
7
|
+
EDGE = "exts"
|
|
8
|
+
GATE = "exte"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class EigerDriverIO(Device):
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -20,7 +20,7 @@ from ophyd_async.core import (
|
|
|
20
20
|
observe_value,
|
|
21
21
|
)
|
|
22
22
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
23
|
-
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw,
|
|
23
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class MotorLimitsException(Exception):
|
|
@@ -63,6 +63,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
63
63
|
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
64
64
|
self.motor_egu = epics_signal_r(str, prefix + ".EGU")
|
|
65
65
|
self.velocity = epics_signal_rw(float, prefix + ".VELO")
|
|
66
|
+
self.offset = epics_signal_rw(float, prefix + ".OFF")
|
|
66
67
|
|
|
67
68
|
with self.add_children_as_readables(Format.HINTED_SIGNAL):
|
|
68
69
|
self.user_readback = epics_signal_r(float, prefix + ".RBV")
|
|
@@ -76,7 +77,10 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
76
77
|
self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
|
|
77
78
|
self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
# Note:cannot use epics_signal_x here, as the motor record specifies that
|
|
81
|
+
# we must write 1 to stop the motor. Simply processing the record is not
|
|
82
|
+
# sufficient.
|
|
83
|
+
self.motor_stop = epics_signal_w(int, prefix + ".STOP")
|
|
80
84
|
# Whether set() should complete successfully or not
|
|
81
85
|
self._set_success = True
|
|
82
86
|
|
|
@@ -91,8 +95,8 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
91
95
|
|
|
92
96
|
super().__init__(name=name)
|
|
93
97
|
|
|
94
|
-
def set_name(self, name: str):
|
|
95
|
-
super().set_name(name)
|
|
98
|
+
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
99
|
+
super().set_name(name, child_name_separator=child_name_separator)
|
|
96
100
|
# Readback should be named the same as its parent in read()
|
|
97
101
|
self.user_readback.set_name(name)
|
|
98
102
|
|
|
@@ -122,9 +126,9 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
122
126
|
@AsyncStatus.wrap
|
|
123
127
|
async def kickoff(self):
|
|
124
128
|
"""Begin moving motor from prepared position to final position."""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
if not self._fly_completed_position:
|
|
130
|
+
msg = "Motor must be prepared before attempting to kickoff"
|
|
131
|
+
raise RuntimeError(msg)
|
|
128
132
|
|
|
129
133
|
self._fly_status = self.set(
|
|
130
134
|
self._fly_completed_position, timeout=self._fly_timeout
|
|
@@ -132,7 +136,9 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
132
136
|
|
|
133
137
|
def complete(self) -> WatchableAsyncStatus:
|
|
134
138
|
"""Mark as complete once motor reaches completed position."""
|
|
135
|
-
|
|
139
|
+
if not self._fly_status:
|
|
140
|
+
msg = "kickoff not called"
|
|
141
|
+
raise RuntimeError(msg)
|
|
136
142
|
return self._fly_status
|
|
137
143
|
|
|
138
144
|
@WatchableAsyncStatus.wrap
|
|
@@ -152,13 +158,18 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
152
158
|
self.velocity.get_value(),
|
|
153
159
|
self.acceleration_time.get_value(),
|
|
154
160
|
)
|
|
161
|
+
|
|
155
162
|
if timeout is CALCULATE_TIMEOUT:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
try:
|
|
164
|
+
timeout = (
|
|
165
|
+
abs((new_position - old_position) / velocity)
|
|
166
|
+
+ 2 * acceleration_time
|
|
167
|
+
+ DEFAULT_TIMEOUT
|
|
168
|
+
)
|
|
169
|
+
except ZeroDivisionError as error:
|
|
170
|
+
msg = "Mover has zero velocity"
|
|
171
|
+
raise ValueError(msg) from error
|
|
172
|
+
|
|
162
173
|
move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
|
|
163
174
|
async for current_position in observe_value(
|
|
164
175
|
self.user_readback, done_status=move_status
|
|
@@ -178,7 +189,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
178
189
|
self._set_success = success
|
|
179
190
|
# Put with completion will never complete as we are waiting for completion on
|
|
180
191
|
# the move above, so need to pass wait=False
|
|
181
|
-
await self.motor_stop.
|
|
192
|
+
await self.motor_stop.set(1, wait=False)
|
|
182
193
|
|
|
183
194
|
async def _prepare_velocity(
|
|
184
195
|
self, start_position: float, end_position: float, time_for_move: float
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ophyd_async.epics.testing import TestingIOC
|
|
5
|
+
|
|
6
|
+
HERE = Path(__file__).absolute().parent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def start_ioc_subprocess(prefix: str, num_counters: int):
|
|
10
|
+
"""Start an IOC subprocess with EPICS database for sample stage and sensor
|
|
11
|
+
with the same pv prefix
|
|
12
|
+
"""
|
|
13
|
+
ioc = TestingIOC()
|
|
14
|
+
# Create X and Y motors
|
|
15
|
+
for suffix in ["X", "Y"]:
|
|
16
|
+
ioc.add_database(HERE / "mover.db", P=f"{prefix}STAGE:{suffix}:")
|
|
17
|
+
# Create a multichannel counter with num_counters
|
|
18
|
+
ioc.add_database(HERE / "multichannelcounter.db", P=f"{prefix}MCC:")
|
|
19
|
+
for i in range(1, num_counters + 1):
|
|
20
|
+
ioc.add_database(
|
|
21
|
+
HERE / "counter.db",
|
|
22
|
+
P=f"{prefix}MCC:",
|
|
23
|
+
CHANNEL=str(i),
|
|
24
|
+
X=f"{prefix}STAGE:X:",
|
|
25
|
+
Y=f"{prefix}STAGE:Y:",
|
|
26
|
+
)
|
|
27
|
+
# Start IOC and register it to be stopped at exit
|
|
28
|
+
ioc.start()
|
|
29
|
+
atexit.register(ioc.stop)
|
|
@@ -37,8 +37,8 @@ class Mover(StandardReadable, Movable, Stoppable):
|
|
|
37
37
|
|
|
38
38
|
super().__init__(name=name)
|
|
39
39
|
|
|
40
|
-
def set_name(self, name: str):
|
|
41
|
-
super().set_name(name)
|
|
40
|
+
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
41
|
+
super().set_name(name, child_name_separator=child_name_separator)
|
|
42
42
|
# Readback should be named the same as its parent in read()
|
|
43
43
|
self.readback.set_name(name)
|
|
44
44
|
|
|
@@ -52,13 +52,19 @@ class Mover(StandardReadable, Movable, Stoppable):
|
|
|
52
52
|
self.precision.get_value(),
|
|
53
53
|
self.velocity.get_value(),
|
|
54
54
|
)
|
|
55
|
-
if timeout
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if timeout is CALCULATE_TIMEOUT:
|
|
56
|
+
try:
|
|
57
|
+
timeout = (
|
|
58
|
+
abs((new_position - old_position) / velocity) + DEFAULT_TIMEOUT
|
|
59
|
+
)
|
|
60
|
+
except ZeroDivisionError as error:
|
|
61
|
+
msg = "Mover has zero velocity"
|
|
62
|
+
raise ValueError(msg) from error
|
|
63
|
+
|
|
58
64
|
# Make an Event that will be set on completion, and a Status that will
|
|
59
65
|
# error if not done in time
|
|
60
66
|
done = asyncio.Event()
|
|
61
|
-
done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout))
|
|
67
|
+
done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout)) # type: ignore
|
|
62
68
|
# Wait for the value to set, but don't wait for put completion callback
|
|
63
69
|
await self.setpoint.set(new_position, wait=False)
|
|
64
70
|
async for current_position in observe_value(
|
|
@@ -15,9 +15,9 @@ class EnergyMode(StrictEnum):
|
|
|
15
15
|
"""Energy mode for `Sensor`"""
|
|
16
16
|
|
|
17
17
|
#: Low energy mode
|
|
18
|
-
|
|
18
|
+
LOW = "Low Energy"
|
|
19
19
|
#: High energy mode
|
|
20
|
-
|
|
20
|
+
HIGH = "High Energy"
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class Sensor(StandardReadable, EpicsDevice):
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from ._example_ioc import (
|
|
2
|
+
CA_PVA_RECORDS,
|
|
3
|
+
PVA_RECORDS,
|
|
4
|
+
EpicsTestCaDevice,
|
|
5
|
+
EpicsTestEnum,
|
|
6
|
+
EpicsTestIocAndDevices,
|
|
7
|
+
EpicsTestPvaDevice,
|
|
8
|
+
EpicsTestSubsetEnum,
|
|
9
|
+
EpicsTestTable,
|
|
10
|
+
)
|
|
11
|
+
from ._utils import TestingIOC, generate_random_pv_prefix
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CA_PVA_RECORDS",
|
|
15
|
+
"PVA_RECORDS",
|
|
16
|
+
"EpicsTestCaDevice",
|
|
17
|
+
"EpicsTestEnum",
|
|
18
|
+
"EpicsTestSubsetEnum",
|
|
19
|
+
"EpicsTestPvaDevice",
|
|
20
|
+
"EpicsTestTable",
|
|
21
|
+
"EpicsTestIocAndDevices",
|
|
22
|
+
"TestingIOC",
|
|
23
|
+
"generate_random_pv_prefix",
|
|
24
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated as A
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ophyd_async.core import Array1D, SignalR, SignalRW, StrictEnum, Table
|
|
8
|
+
from ophyd_async.core._utils import SubsetEnum
|
|
9
|
+
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
10
|
+
|
|
11
|
+
from ._utils import TestingIOC, generate_random_pv_prefix
|
|
12
|
+
|
|
13
|
+
CA_PVA_RECORDS = Path(__file__).parent / "test_records.db"
|
|
14
|
+
PVA_RECORDS = Path(__file__).parent / "test_records_pva.db"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EpicsTestEnum(StrictEnum):
|
|
18
|
+
A = "Aaa"
|
|
19
|
+
B = "Bbb"
|
|
20
|
+
C = "Ccc"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EpicsTestSubsetEnum(SubsetEnum):
|
|
24
|
+
A = "Aaa"
|
|
25
|
+
B = "Bbb"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EpicsTestTable(Table):
|
|
29
|
+
bool: Array1D[np.bool_]
|
|
30
|
+
int: Array1D[np.int32]
|
|
31
|
+
float: Array1D[np.float64]
|
|
32
|
+
str: Sequence[str]
|
|
33
|
+
enum: Sequence[EpicsTestEnum]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EpicsTestCaDevice(EpicsDevice):
|
|
37
|
+
my_int: A[SignalRW[int], PvSuffix("int")]
|
|
38
|
+
my_float: A[SignalRW[float], PvSuffix("float")]
|
|
39
|
+
float_prec_0: A[SignalRW[int], PvSuffix("float_prec_0")]
|
|
40
|
+
my_str: A[SignalRW[str], PvSuffix("str")]
|
|
41
|
+
longstr: A[SignalRW[str], PvSuffix("longstr")]
|
|
42
|
+
longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
|
|
43
|
+
my_bool: A[SignalRW[bool], PvSuffix("bool")]
|
|
44
|
+
enum: A[SignalRW[EpicsTestEnum], PvSuffix("enum")]
|
|
45
|
+
enum2: A[SignalRW[EpicsTestEnum], PvSuffix("enum2")]
|
|
46
|
+
subset_enum: A[SignalRW[EpicsTestSubsetEnum], PvSuffix("subset_enum")]
|
|
47
|
+
enum_str_fallback: A[SignalRW[str], PvSuffix("enum_str_fallback")]
|
|
48
|
+
bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
|
|
49
|
+
partialint: A[SignalRW[int], PvSuffix("partialint")]
|
|
50
|
+
lessint: A[SignalRW[int], PvSuffix("lessint")]
|
|
51
|
+
uint8a: A[SignalRW[Array1D[np.uint8]], PvSuffix("uint8a")]
|
|
52
|
+
int16a: A[SignalRW[Array1D[np.int16]], PvSuffix("int16a")]
|
|
53
|
+
int32a: A[SignalRW[Array1D[np.int32]], PvSuffix("int32a")]
|
|
54
|
+
float32a: A[SignalRW[Array1D[np.float32]], PvSuffix("float32a")]
|
|
55
|
+
float64a: A[SignalRW[Array1D[np.float64]], PvSuffix("float64a")]
|
|
56
|
+
stra: A[SignalRW[Sequence[str]], PvSuffix("stra")]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EpicsTestPvaDevice(EpicsTestCaDevice):
|
|
60
|
+
# pva can support all signal types that ca can
|
|
61
|
+
int8a: A[SignalRW[Array1D[np.int8]], PvSuffix("int8a")]
|
|
62
|
+
uint16a: A[SignalRW[Array1D[np.uint16]], PvSuffix("uint16a")]
|
|
63
|
+
uint32a: A[SignalRW[Array1D[np.uint32]], PvSuffix("uint32a")]
|
|
64
|
+
int64a: A[SignalRW[Array1D[np.int64]], PvSuffix("int64a")]
|
|
65
|
+
uint64a: A[SignalRW[Array1D[np.uint64]], PvSuffix("uint64a")]
|
|
66
|
+
table: A[SignalRW[EpicsTestTable], PvSuffix("table")]
|
|
67
|
+
ntndarray: A[SignalR[np.ndarray], PvSuffix("ntndarray")]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class EpicsTestIocAndDevices:
|
|
71
|
+
def __init__(self):
|
|
72
|
+
self.prefix = generate_random_pv_prefix()
|
|
73
|
+
self.ioc = TestingIOC()
|
|
74
|
+
# Create supporting records and ExampleCaDevice
|
|
75
|
+
ca_prefix = f"{self.prefix}ca:"
|
|
76
|
+
self.ioc.add_database(CA_PVA_RECORDS, device=ca_prefix)
|
|
77
|
+
self.ca_device = EpicsTestCaDevice(f"ca://{ca_prefix}")
|
|
78
|
+
# Create supporting records and ExamplePvaDevice
|
|
79
|
+
pva_prefix = f"{self.prefix}pva:"
|
|
80
|
+
self.ioc.add_database(CA_PVA_RECORDS, device=pva_prefix)
|
|
81
|
+
self.ioc.add_database(PVA_RECORDS, device=pva_prefix)
|
|
82
|
+
self.pva_device = EpicsTestPvaDevice(f"pva://{pva_prefix}")
|
|
83
|
+
|
|
84
|
+
def get_device(self, protocol: str) -> EpicsTestCaDevice | EpicsTestPvaDevice:
|
|
85
|
+
return getattr(self, f"{protocol}_device")
|
|
86
|
+
|
|
87
|
+
def get_signal(self, protocol: str, name: str) -> SignalRW:
|
|
88
|
+
return getattr(self.get_device(protocol), name)
|
|
89
|
+
|
|
90
|
+
def get_pv(self, protocol: str, name: str) -> str:
|
|
91
|
+
return f"{protocol}://{self.prefix}{protocol}:{name}"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import string
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_random_pv_prefix() -> str:
|
|
10
|
+
return "".join(random.choice(string.ascii_lowercase) for _ in range(12)) + ":"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestingIOC:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self._db_macros: list[tuple[Path, dict[str, str]]] = []
|
|
16
|
+
self.output = ""
|
|
17
|
+
|
|
18
|
+
def add_database(self, db: Path | str, /, **macros: str):
|
|
19
|
+
self._db_macros.append((Path(db), macros))
|
|
20
|
+
|
|
21
|
+
def start(self):
|
|
22
|
+
ioc_args = [
|
|
23
|
+
sys.executable,
|
|
24
|
+
"-m",
|
|
25
|
+
"epicscorelibs.ioc",
|
|
26
|
+
]
|
|
27
|
+
for db, macros in self._db_macros:
|
|
28
|
+
macro_str = ",".join(f"{k}={v}" for k, v in macros.items())
|
|
29
|
+
ioc_args += ["-m", macro_str, "-d", str(db)]
|
|
30
|
+
self._process = subprocess.Popen(
|
|
31
|
+
ioc_args,
|
|
32
|
+
stdin=subprocess.PIPE,
|
|
33
|
+
stdout=subprocess.PIPE,
|
|
34
|
+
stderr=subprocess.STDOUT,
|
|
35
|
+
universal_newlines=True,
|
|
36
|
+
)
|
|
37
|
+
assert self._process.stdout # noqa: S101 # this is to make Pylance happy
|
|
38
|
+
start_time = time.monotonic()
|
|
39
|
+
while "iocRun: All initialization complete" not in self.output:
|
|
40
|
+
if time.monotonic() - start_time > 10:
|
|
41
|
+
self.stop()
|
|
42
|
+
raise TimeoutError(f"IOC did not start in time:\n{self.output}")
|
|
43
|
+
self.output += self._process.stdout.readline()
|
|
44
|
+
|
|
45
|
+
def stop(self):
|
|
46
|
+
try:
|
|
47
|
+
self.output += self._process.communicate("exit()")[0]
|
|
48
|
+
except ValueError:
|
|
49
|
+
# Someone else already called communicate
|
|
50
|
+
pass
|