ophyd-async 0.6.0__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/_version.py +2 -2
- ophyd_async/core/__init__.py +4 -4
- ophyd_async/core/_detector.py +74 -37
- ophyd_async/core/_device.py +6 -1
- ophyd_async/core/_flyer.py +5 -20
- ophyd_async/core/_table.py +101 -18
- ophyd_async/epics/adaravis/_aravis_controller.py +4 -4
- ophyd_async/epics/adcore/_core_logic.py +2 -2
- ophyd_async/epics/adkinetix/_kinetix_controller.py +3 -3
- ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -3
- ophyd_async/epics/adsimdetector/_sim.py +1 -1
- ophyd_async/epics/adsimdetector/_sim_controller.py +3 -3
- ophyd_async/epics/advimba/_vimba_controller.py +3 -3
- ophyd_async/epics/eiger/_eiger_controller.py +3 -3
- ophyd_async/fastcs/panda/_block.py +7 -0
- ophyd_async/fastcs/panda/_control.py +2 -2
- ophyd_async/fastcs/panda/_table.py +3 -37
- ophyd_async/fastcs/panda/_trigger.py +3 -3
- ophyd_async/fastcs/panda/_writer.py +2 -2
- ophyd_async/plan_stubs/_fly.py +1 -3
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +5 -3
- 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.6.0.dist-info → ophyd_async-0.7.0.dist-info}/METADATA +5 -1
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/RECORD +40 -28
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/WHEEL +1 -1
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -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]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
|
-
|
|
4
|
+
DetectorController,
|
|
5
5
|
DetectorTrigger,
|
|
6
6
|
wait_for_value,
|
|
7
7
|
)
|
|
@@ -11,7 +11,7 @@ from ophyd_async.core._status import AsyncStatus
|
|
|
11
11
|
from ._block import PcapBlock
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class PandaPcapController(
|
|
14
|
+
class PandaPcapController(DetectorController):
|
|
15
15
|
def __init__(self, pcap: PcapBlock) -> None:
|
|
16
16
|
self.pcap = pcap
|
|
17
17
|
self._arm_status: AsyncStatus | None = None
|
|
@@ -4,7 +4,7 @@ from typing import Annotated
|
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import numpy.typing as npt
|
|
7
|
-
from pydantic import Field,
|
|
7
|
+
from pydantic import Field, model_validator
|
|
8
8
|
from pydantic_numpy.helper.annotation import NpArrayPydanticAnnotation
|
|
9
9
|
from typing_extensions import TypedDict
|
|
10
10
|
|
|
@@ -51,13 +51,7 @@ PydanticNp1DArrayBool = Annotated[
|
|
|
51
51
|
),
|
|
52
52
|
Field(default_factory=lambda: np.array([], dtype=np.bool_)),
|
|
53
53
|
]
|
|
54
|
-
TriggerStr = Annotated[
|
|
55
|
-
np.ndarray[tuple[int], np.dtype[np.unicode_]],
|
|
56
|
-
NpArrayPydanticAnnotation.factory(
|
|
57
|
-
data_type=np.unicode_, dimensions=1, strict_data_typing=False
|
|
58
|
-
),
|
|
59
|
-
Field(default_factory=lambda: np.array([], dtype=np.dtype("<U32"))),
|
|
60
|
-
]
|
|
54
|
+
TriggerStr = Annotated[Sequence[SeqTrigger], Field(default_factory=list)]
|
|
61
55
|
|
|
62
56
|
|
|
63
57
|
class SeqTable(Table):
|
|
@@ -101,35 +95,7 @@ class SeqTable(Table):
|
|
|
101
95
|
oute2: bool = False,
|
|
102
96
|
outf2: bool = False,
|
|
103
97
|
) -> "SeqTable":
|
|
104
|
-
|
|
105
|
-
trigger = trigger.value
|
|
106
|
-
return super().row(**locals())
|
|
107
|
-
|
|
108
|
-
@field_validator("trigger", mode="before")
|
|
109
|
-
@classmethod
|
|
110
|
-
def trigger_to_np_array(cls, trigger_column):
|
|
111
|
-
"""
|
|
112
|
-
The user can provide a list of SeqTrigger enum elements instead of a numpy str.
|
|
113
|
-
"""
|
|
114
|
-
if isinstance(trigger_column, Sequence) and all(
|
|
115
|
-
isinstance(trigger, SeqTrigger) for trigger in trigger_column
|
|
116
|
-
):
|
|
117
|
-
trigger_column = np.array(
|
|
118
|
-
[trigger.value for trigger in trigger_column], dtype=np.dtype("<U32")
|
|
119
|
-
)
|
|
120
|
-
elif isinstance(trigger_column, Sequence) or isinstance(
|
|
121
|
-
trigger_column, np.ndarray
|
|
122
|
-
):
|
|
123
|
-
for trigger in trigger_column:
|
|
124
|
-
SeqTrigger(
|
|
125
|
-
trigger
|
|
126
|
-
) # To check all the given strings are actually `SeqTrigger`s
|
|
127
|
-
else:
|
|
128
|
-
raise ValueError(
|
|
129
|
-
"Expected a numpy array or a sequence of `SeqTrigger`, got "
|
|
130
|
-
f"{type(trigger_column)}."
|
|
131
|
-
)
|
|
132
|
-
return trigger_column
|
|
98
|
+
return Table.row(**locals())
|
|
133
99
|
|
|
134
100
|
@model_validator(mode="after")
|
|
135
101
|
def validate_max_length(self) -> "SeqTable":
|
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
-
from ophyd_async.core import
|
|
5
|
+
from ophyd_async.core import FlyerController, wait_for_value
|
|
6
6
|
|
|
7
7
|
from ._block import PcompBlock, PcompDirectionOptions, SeqBlock, TimeUnits
|
|
8
8
|
from ._table import SeqTable
|
|
@@ -14,7 +14,7 @@ class SeqTableInfo(BaseModel):
|
|
|
14
14
|
prescale_as_us: float = Field(default=1, ge=0) # microseconds
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class StaticSeqTableTriggerLogic(
|
|
17
|
+
class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
|
|
18
18
|
def __init__(self, seq: SeqBlock) -> None:
|
|
19
19
|
self.seq = seq
|
|
20
20
|
|
|
@@ -63,7 +63,7 @@ class PcompInfo(BaseModel):
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
class StaticPcompTriggerLogic(
|
|
66
|
+
class StaticPcompTriggerLogic(FlyerController[PcompInfo]):
|
|
67
67
|
def __init__(self, pcomp: PcompBlock) -> None:
|
|
68
68
|
self.pcomp = pcomp
|
|
69
69
|
|
|
@@ -17,7 +17,7 @@ from ophyd_async.core import (
|
|
|
17
17
|
wait_for_value,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
from ._block import DataBlock
|
|
20
|
+
from ._block import CaptureMode, DataBlock
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class PandaHDFWriter(DetectorWriter):
|
|
@@ -58,7 +58,7 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
58
58
|
self.panda_data_block.hdf_file_name.set(
|
|
59
59
|
f"{info.filename}.h5",
|
|
60
60
|
),
|
|
61
|
-
self.panda_data_block.
|
|
61
|
+
self.panda_data_block.capture_mode.set(CaptureMode.FOREVER),
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
# Make sure that directory exists or has been created.
|
ophyd_async/plan_stubs/_fly.py
CHANGED
|
@@ -44,7 +44,6 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
|
44
44
|
repeats: int = 1,
|
|
45
45
|
period: float = 0.0,
|
|
46
46
|
frame_timeout: float | None = None,
|
|
47
|
-
iteration: int = 1,
|
|
48
47
|
):
|
|
49
48
|
"""Prepare a hardware triggered flyable and one or more detectors.
|
|
50
49
|
|
|
@@ -62,12 +61,11 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
|
62
61
|
deadtime = max(det.controller.get_deadtime(exposure) for det in detectors)
|
|
63
62
|
|
|
64
63
|
trigger_info = TriggerInfo(
|
|
65
|
-
|
|
64
|
+
number_of_triggers=number_of_frames * repeats,
|
|
66
65
|
trigger=DetectorTrigger.constant_gate,
|
|
67
66
|
deadtime=deadtime,
|
|
68
67
|
livetime=exposure,
|
|
69
68
|
frame_timeout=frame_timeout,
|
|
70
|
-
iteration=iteration,
|
|
71
69
|
)
|
|
72
70
|
trigger_time = number_of_frames * (exposure + deadtime)
|
|
73
71
|
pre_delay = max(period - 2 * shutter_time - trigger_time, 0)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
3
|
+
from ophyd_async.core import DetectorController, PathProvider
|
|
4
4
|
from ophyd_async.core._detector import TriggerInfo
|
|
5
5
|
|
|
6
6
|
from ._pattern_generator import PatternGenerator
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class PatternDetectorController(
|
|
9
|
+
class PatternDetectorController(DetectorController):
|
|
10
10
|
def __init__(
|
|
11
11
|
self,
|
|
12
12
|
pattern_generator: PatternGenerator,
|
|
@@ -32,7 +32,9 @@ class PatternDetectorController(DetectorControl):
|
|
|
32
32
|
assert self.period
|
|
33
33
|
self.task = asyncio.create_task(
|
|
34
34
|
self._coroutine_for_image_writing(
|
|
35
|
-
self._trigger_info.livetime,
|
|
35
|
+
self._trigger_info.livetime,
|
|
36
|
+
self.period,
|
|
37
|
+
self._trigger_info.total_number_of_triggers,
|
|
36
38
|
)
|
|
37
39
|
)
|
|
38
40
|
|
ophyd_async/tango/__init__.py
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from .base_devices import (
|
|
2
|
+
TangoDevice,
|
|
3
|
+
TangoReadable,
|
|
4
|
+
tango_polling,
|
|
5
|
+
)
|
|
6
|
+
from .signal import (
|
|
7
|
+
AttributeProxy,
|
|
8
|
+
CommandProxy,
|
|
9
|
+
TangoSignalBackend,
|
|
10
|
+
__tango_signal_auto,
|
|
11
|
+
ensure_proper_executor,
|
|
12
|
+
get_dtype_extended,
|
|
13
|
+
get_python_type,
|
|
14
|
+
get_tango_trl,
|
|
15
|
+
get_trl_descriptor,
|
|
16
|
+
infer_python_type,
|
|
17
|
+
infer_signal_character,
|
|
18
|
+
make_backend,
|
|
19
|
+
tango_signal_r,
|
|
20
|
+
tango_signal_rw,
|
|
21
|
+
tango_signal_w,
|
|
22
|
+
tango_signal_x,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"TangoDevice",
|
|
27
|
+
"TangoReadable",
|
|
28
|
+
"tango_polling",
|
|
29
|
+
"TangoSignalBackend",
|
|
30
|
+
"get_python_type",
|
|
31
|
+
"get_dtype_extended",
|
|
32
|
+
"get_trl_descriptor",
|
|
33
|
+
"get_tango_trl",
|
|
34
|
+
"infer_python_type",
|
|
35
|
+
"infer_signal_character",
|
|
36
|
+
"make_backend",
|
|
37
|
+
"AttributeProxy",
|
|
38
|
+
"CommandProxy",
|
|
39
|
+
"ensure_proper_executor",
|
|
40
|
+
"__tango_signal_auto",
|
|
41
|
+
"tango_signal_r",
|
|
42
|
+
"tango_signal_rw",
|
|
43
|
+
"tango_signal_w",
|
|
44
|
+
"tango_signal_x",
|
|
45
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
TypeVar,
|
|
5
|
+
get_args,
|
|
6
|
+
get_origin,
|
|
7
|
+
get_type_hints,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from ophyd_async.core import (
|
|
11
|
+
DEFAULT_TIMEOUT,
|
|
12
|
+
Device,
|
|
13
|
+
Signal,
|
|
14
|
+
)
|
|
15
|
+
from ophyd_async.tango.signal import (
|
|
16
|
+
TangoSignalBackend,
|
|
17
|
+
__tango_signal_auto,
|
|
18
|
+
make_backend,
|
|
19
|
+
)
|
|
20
|
+
from tango import DeviceProxy as DeviceProxy
|
|
21
|
+
from tango.asyncio import DeviceProxy as AsyncDeviceProxy
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TangoDevice(Device):
|
|
27
|
+
"""
|
|
28
|
+
General class for TangoDevices. Extends Device to provide attributes for Tango
|
|
29
|
+
devices.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
trl: str
|
|
34
|
+
Tango resource locator, typically of the device server.
|
|
35
|
+
device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]]
|
|
36
|
+
Asynchronous or synchronous DeviceProxy object for the device. If not provided,
|
|
37
|
+
an asynchronous DeviceProxy object will be created using the trl and awaited
|
|
38
|
+
when the device is connected.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
trl: str = ""
|
|
42
|
+
proxy: DeviceProxy | None = None
|
|
43
|
+
_polling: tuple[bool, float, float | None, float | None] = (False, 0.1, None, 0.1)
|
|
44
|
+
_signal_polling: dict[str, tuple[bool, float, float, float]] = {}
|
|
45
|
+
_poll_only_annotated_signals: bool = True
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
trl: str | None = None,
|
|
50
|
+
device_proxy: DeviceProxy | None = None,
|
|
51
|
+
name: str = "",
|
|
52
|
+
) -> None:
|
|
53
|
+
self.trl = trl if trl else ""
|
|
54
|
+
self.proxy = device_proxy
|
|
55
|
+
tango_create_children_from_annotations(self)
|
|
56
|
+
super().__init__(name=name)
|
|
57
|
+
|
|
58
|
+
def set_trl(self, trl: str):
|
|
59
|
+
"""Set the Tango resource locator."""
|
|
60
|
+
if not isinstance(trl, str):
|
|
61
|
+
raise ValueError("TRL must be a string.")
|
|
62
|
+
self.trl = trl
|
|
63
|
+
|
|
64
|
+
async def connect(
|
|
65
|
+
self,
|
|
66
|
+
mock: bool = False,
|
|
67
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
68
|
+
force_reconnect: bool = False,
|
|
69
|
+
):
|
|
70
|
+
if self.trl and self.proxy is None:
|
|
71
|
+
self.proxy = await AsyncDeviceProxy(self.trl)
|
|
72
|
+
elif self.proxy and not self.trl:
|
|
73
|
+
self.trl = self.proxy.name()
|
|
74
|
+
|
|
75
|
+
# Set the trl of the signal backends
|
|
76
|
+
for child in self.children():
|
|
77
|
+
if isinstance(child[1], Signal):
|
|
78
|
+
if isinstance(child[1]._backend, TangoSignalBackend): # noqa: SLF001
|
|
79
|
+
resource_name = child[0].lstrip("_")
|
|
80
|
+
read_trl = f"{self.trl}/{resource_name}"
|
|
81
|
+
child[1]._backend.set_trl(read_trl, read_trl) # noqa: SLF001
|
|
82
|
+
|
|
83
|
+
if self.proxy is not None:
|
|
84
|
+
self.register_signals()
|
|
85
|
+
await _fill_proxy_entries(self)
|
|
86
|
+
|
|
87
|
+
# set_name should be called again to propagate the new signal names
|
|
88
|
+
self.set_name(self.name)
|
|
89
|
+
|
|
90
|
+
# Set the polling configuration
|
|
91
|
+
if self._polling[0]:
|
|
92
|
+
for child in self.children():
|
|
93
|
+
child_type = type(child[1])
|
|
94
|
+
if issubclass(child_type, Signal):
|
|
95
|
+
if isinstance(child[1]._backend, TangoSignalBackend): # noqa: SLF001 # type: ignore
|
|
96
|
+
child[1]._backend.set_polling(*self._polling) # noqa: SLF001 # type: ignore
|
|
97
|
+
child[1]._backend.allow_events(False) # noqa: SLF001 # type: ignore
|
|
98
|
+
if self._signal_polling:
|
|
99
|
+
for signal_name, polling in self._signal_polling.items():
|
|
100
|
+
if hasattr(self, signal_name):
|
|
101
|
+
attr = getattr(self, signal_name)
|
|
102
|
+
if isinstance(attr._backend, TangoSignalBackend): # noqa: SLF001
|
|
103
|
+
attr._backend.set_polling(*polling) # noqa: SLF001
|
|
104
|
+
attr._backend.allow_events(False) # noqa: SLF001
|
|
105
|
+
|
|
106
|
+
await super().connect(mock=mock, timeout=timeout)
|
|
107
|
+
|
|
108
|
+
# Users can override this method to register new signals
|
|
109
|
+
def register_signals(self):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def tango_polling(
|
|
114
|
+
polling: tuple[float, float, float]
|
|
115
|
+
| dict[str, tuple[float, float, float]]
|
|
116
|
+
| None = None,
|
|
117
|
+
signal_polling: dict[str, tuple[float, float, float]] | None = None,
|
|
118
|
+
):
|
|
119
|
+
"""
|
|
120
|
+
Class decorator to configure polling for Tango devices.
|
|
121
|
+
|
|
122
|
+
This decorator allows for the configuration of both device-level and signal-level
|
|
123
|
+
polling for Tango devices. Polling is useful for device servers that do not support
|
|
124
|
+
event-driven updates.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
polling : Optional[Union[Tuple[float, float, float],
|
|
129
|
+
Dict[str, Tuple[float, float, float]]]], optional
|
|
130
|
+
Device-level polling configuration as a tuple of three floats representing the
|
|
131
|
+
polling interval, polling timeout, and polling delay. Alternatively,
|
|
132
|
+
a dictionary can be provided to specify signal-level polling configurations
|
|
133
|
+
directly.
|
|
134
|
+
signal_polling : Optional[Dict[str, Tuple[float, float, float]]], optional
|
|
135
|
+
Signal-level polling configuration as a dictionary where keys are signal names
|
|
136
|
+
and values are tuples of three floats representing the polling interval, polling
|
|
137
|
+
timeout, and polling delay.
|
|
138
|
+
"""
|
|
139
|
+
if isinstance(polling, dict):
|
|
140
|
+
signal_polling = polling
|
|
141
|
+
polling = None
|
|
142
|
+
|
|
143
|
+
def decorator(cls):
|
|
144
|
+
if polling is not None:
|
|
145
|
+
cls._polling = (True, *polling)
|
|
146
|
+
if signal_polling is not None:
|
|
147
|
+
cls._signal_polling = {k: (True, *v) for k, v in signal_polling.items()}
|
|
148
|
+
return cls
|
|
149
|
+
|
|
150
|
+
return decorator
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def tango_create_children_from_annotations(
|
|
154
|
+
device: TangoDevice, included_optional_fields: tuple[str, ...] = ()
|
|
155
|
+
):
|
|
156
|
+
"""Initialize blocks at __init__ of `device`."""
|
|
157
|
+
for name, device_type in get_type_hints(type(device)).items():
|
|
158
|
+
if name in ("_name", "parent"):
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# device_type, is_optional = _strip_union(device_type)
|
|
162
|
+
# if is_optional and name not in included_optional_fields:
|
|
163
|
+
# continue
|
|
164
|
+
#
|
|
165
|
+
# is_device_vector, device_type = _strip_device_vector(device_type)
|
|
166
|
+
# if is_device_vector:
|
|
167
|
+
# n_device_vector = DeviceVector()
|
|
168
|
+
# setattr(device, name, n_device_vector)
|
|
169
|
+
|
|
170
|
+
# else:
|
|
171
|
+
origin = get_origin(device_type)
|
|
172
|
+
origin = origin if origin else device_type
|
|
173
|
+
|
|
174
|
+
if issubclass(origin, Signal):
|
|
175
|
+
type_args = get_args(device_type)
|
|
176
|
+
datatype = type_args[0] if type_args else None
|
|
177
|
+
backend = make_backend(datatype=datatype, device_proxy=device.proxy)
|
|
178
|
+
setattr(device, name, origin(name=name, backend=backend))
|
|
179
|
+
|
|
180
|
+
elif issubclass(origin, Device) or isinstance(origin, Device):
|
|
181
|
+
assert callable(origin), f"{origin} is not callable."
|
|
182
|
+
setattr(device, name, origin())
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def _fill_proxy_entries(device: TangoDevice):
|
|
186
|
+
if device.proxy is None:
|
|
187
|
+
raise RuntimeError(f"Device proxy is not connected for {device.name}")
|
|
188
|
+
proxy_trl = device.trl
|
|
189
|
+
children = [name.lstrip("_") for name, _ in device.children()]
|
|
190
|
+
proxy_attributes = list(device.proxy.get_attribute_list())
|
|
191
|
+
proxy_commands = list(device.proxy.get_command_list())
|
|
192
|
+
combined = proxy_attributes + proxy_commands
|
|
193
|
+
|
|
194
|
+
for name in combined:
|
|
195
|
+
if name not in children:
|
|
196
|
+
full_trl = f"{proxy_trl}/{name}"
|
|
197
|
+
try:
|
|
198
|
+
auto_signal = await __tango_signal_auto(
|
|
199
|
+
trl=full_trl, device_proxy=device.proxy
|
|
200
|
+
)
|
|
201
|
+
setattr(device, name, auto_signal)
|
|
202
|
+
except RuntimeError as e:
|
|
203
|
+
if "Commands with different in and out dtypes" in str(e):
|
|
204
|
+
print(
|
|
205
|
+
f"Skipping {name}. Commands with different in and out dtypes"
|
|
206
|
+
f" are not supported."
|
|
207
|
+
)
|
|
208
|
+
continue
|
|
209
|
+
raise e
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# def _strip_union(field: T | T) -> tuple[T, bool]:
|
|
213
|
+
# if get_origin(field) is Union:
|
|
214
|
+
# args = get_args(field)
|
|
215
|
+
# is_optional = type(None) in args
|
|
216
|
+
# for arg in args:
|
|
217
|
+
# if arg is not type(None):
|
|
218
|
+
# return arg, is_optional
|
|
219
|
+
# return field, False
|
|
220
|
+
#
|
|
221
|
+
#
|
|
222
|
+
# def _strip_device_vector(field: type[Device]) -> tuple[bool, type[Device]]:
|
|
223
|
+
# if get_origin(field) is DeviceVector:
|
|
224
|
+
# return True, get_args(field)[0]
|
|
225
|
+
# return False, field
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
StandardReadable,
|
|
5
|
+
)
|
|
6
|
+
from ophyd_async.tango.base_devices._base_device import TangoDevice
|
|
7
|
+
from tango import DeviceProxy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TangoReadable(TangoDevice, StandardReadable):
|
|
11
|
+
"""
|
|
12
|
+
General class for readable TangoDevices. Extends StandardReadable to provide
|
|
13
|
+
attributes for Tango devices.
|
|
14
|
+
|
|
15
|
+
Usage: to proper signals mount should be awaited:
|
|
16
|
+
new_device = await TangoDevice(<tango_device>)
|
|
17
|
+
|
|
18
|
+
Attributes
|
|
19
|
+
----------
|
|
20
|
+
trl : str
|
|
21
|
+
Tango resource locator, typically of the device server.
|
|
22
|
+
proxy : AsyncDeviceProxy
|
|
23
|
+
AsyncDeviceProxy object for the device. This is created when the
|
|
24
|
+
device is connected.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
trl: str | None = None,
|
|
30
|
+
device_proxy: DeviceProxy | None = None,
|
|
31
|
+
name: str = "",
|
|
32
|
+
) -> None:
|
|
33
|
+
TangoDevice.__init__(self, trl, device_proxy=device_proxy, name=name)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from ._counter import TangoCounter
|
|
2
|
+
from ._detector import TangoDetector
|
|
3
|
+
from ._mover import TangoMover
|
|
4
|
+
from ._tango import DemoCounter, DemoMover
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"DemoCounter",
|
|
8
|
+
"DemoMover",
|
|
9
|
+
"TangoCounter",
|
|
10
|
+
"TangoMover",
|
|
11
|
+
"TangoDetector",
|
|
12
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from ophyd_async.core import (
|
|
2
|
+
DEFAULT_TIMEOUT,
|
|
3
|
+
AsyncStatus,
|
|
4
|
+
ConfigSignal,
|
|
5
|
+
HintedSignal,
|
|
6
|
+
SignalR,
|
|
7
|
+
SignalRW,
|
|
8
|
+
SignalX,
|
|
9
|
+
)
|
|
10
|
+
from ophyd_async.tango import TangoReadable, tango_polling
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Enable device level polling, useful for servers that do not support events
|
|
14
|
+
# Polling for individual signal can be enabled with a dict
|
|
15
|
+
@tango_polling({"counts": (1.0, 0.1, 0.1), "sample_time": (0.1, 0.1, 0.1)})
|
|
16
|
+
class TangoCounter(TangoReadable):
|
|
17
|
+
# Enter the name and type of the signals you want to use
|
|
18
|
+
# If type is None or Signal, the type will be inferred from the Tango device
|
|
19
|
+
counts: SignalR[int]
|
|
20
|
+
sample_time: SignalRW[float]
|
|
21
|
+
start: SignalX
|
|
22
|
+
_reset: SignalX
|
|
23
|
+
|
|
24
|
+
def __init__(self, trl: str | None = "", name=""):
|
|
25
|
+
super().__init__(trl, name=name)
|
|
26
|
+
self.add_readables([self.counts], HintedSignal)
|
|
27
|
+
self.add_readables([self.sample_time], ConfigSignal)
|
|
28
|
+
|
|
29
|
+
@AsyncStatus.wrap
|
|
30
|
+
async def trigger(self) -> None:
|
|
31
|
+
sample_time = await self.sample_time.get_value()
|
|
32
|
+
timeout = sample_time + DEFAULT_TIMEOUT
|
|
33
|
+
await self.start.trigger(wait=True, timeout=timeout)
|
|
34
|
+
|
|
35
|
+
@AsyncStatus.wrap
|
|
36
|
+
async def reset(self) -> None:
|
|
37
|
+
await self._reset.trigger(wait=True, timeout=DEFAULT_TIMEOUT)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
AsyncStatus,
|
|
5
|
+
DeviceVector,
|
|
6
|
+
StandardReadable,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from ._counter import TangoCounter
|
|
10
|
+
from ._mover import TangoMover
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TangoDetector(StandardReadable):
|
|
14
|
+
def __init__(self, mover_trl: str, counter_trls: list[str], name=""):
|
|
15
|
+
# A detector device may be composed of tango sub-devices
|
|
16
|
+
self.mover = TangoMover(mover_trl)
|
|
17
|
+
self.counters = DeviceVector(
|
|
18
|
+
{i + 1: TangoCounter(c_trl) for i, c_trl in enumerate(counter_trls)}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Define the readables for TangoDetector
|
|
22
|
+
# DeviceVectors are incompatible with AsyncReadable. Ignore until fixed.
|
|
23
|
+
self.add_readables([self.counters, self.mover]) # type: ignore
|
|
24
|
+
|
|
25
|
+
super().__init__(name=name)
|
|
26
|
+
|
|
27
|
+
def set(self, value):
|
|
28
|
+
return self.mover.set(value)
|
|
29
|
+
|
|
30
|
+
def stop(self, success: bool = True) -> AsyncStatus:
|
|
31
|
+
return self.mover.stop(success)
|
|
32
|
+
|
|
33
|
+
@AsyncStatus.wrap
|
|
34
|
+
async def trigger(self):
|
|
35
|
+
statuses = []
|
|
36
|
+
for counter in self.counters.values():
|
|
37
|
+
statuses.append(counter.reset())
|
|
38
|
+
await asyncio.gather(*statuses)
|
|
39
|
+
statuses.clear()
|
|
40
|
+
for counter in self.counters.values():
|
|
41
|
+
statuses.append(counter.trigger())
|
|
42
|
+
await asyncio.gather(*statuses)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from bluesky.protocols import Movable, Stoppable
|
|
4
|
+
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
CALCULATE_TIMEOUT,
|
|
7
|
+
DEFAULT_TIMEOUT,
|
|
8
|
+
AsyncStatus,
|
|
9
|
+
CalculatableTimeout,
|
|
10
|
+
ConfigSignal,
|
|
11
|
+
HintedSignal,
|
|
12
|
+
SignalR,
|
|
13
|
+
SignalRW,
|
|
14
|
+
SignalX,
|
|
15
|
+
WatchableAsyncStatus,
|
|
16
|
+
WatcherUpdate,
|
|
17
|
+
observe_value,
|
|
18
|
+
wait_for_value,
|
|
19
|
+
)
|
|
20
|
+
from ophyd_async.tango import TangoReadable, tango_polling
|
|
21
|
+
from tango import DevState
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Enable device level polling, useful for servers that do not support events
|
|
25
|
+
@tango_polling((0.1, 0.1, 0.1))
|
|
26
|
+
class TangoMover(TangoReadable, Movable, Stoppable):
|
|
27
|
+
# Enter the name and type of the signals you want to use
|
|
28
|
+
# If type is None or Signal, the type will be inferred from the Tango device
|
|
29
|
+
position: SignalRW[float]
|
|
30
|
+
velocity: SignalRW[float]
|
|
31
|
+
state: SignalR[DevState]
|
|
32
|
+
_stop: SignalX
|
|
33
|
+
|
|
34
|
+
def __init__(self, trl: str | None = "", name=""):
|
|
35
|
+
super().__init__(trl, name=name)
|
|
36
|
+
self.add_readables([self.position], HintedSignal)
|
|
37
|
+
self.add_readables([self.velocity], ConfigSignal)
|
|
38
|
+
self._set_success = True
|
|
39
|
+
|
|
40
|
+
@WatchableAsyncStatus.wrap
|
|
41
|
+
async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
|
|
42
|
+
self._set_success = True
|
|
43
|
+
(old_position, velocity) = await asyncio.gather(
|
|
44
|
+
self.position.get_value(), self.velocity.get_value()
|
|
45
|
+
)
|
|
46
|
+
if timeout is CALCULATE_TIMEOUT:
|
|
47
|
+
assert velocity > 0, "Motor has zero velocity"
|
|
48
|
+
timeout = abs(value - old_position) / velocity + DEFAULT_TIMEOUT
|
|
49
|
+
|
|
50
|
+
if not (isinstance(timeout, float) or timeout is None):
|
|
51
|
+
raise ValueError("Timeout must be a float or None")
|
|
52
|
+
# For this server, set returns immediately so this status should not be awaited
|
|
53
|
+
await self.position.set(value, wait=False, timeout=timeout)
|
|
54
|
+
|
|
55
|
+
move_status = AsyncStatus(
|
|
56
|
+
wait_for_value(self.state, DevState.ON, timeout=timeout)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
async for current_position in observe_value(
|
|
61
|
+
self.position, done_status=move_status
|
|
62
|
+
):
|
|
63
|
+
yield WatcherUpdate(
|
|
64
|
+
current=current_position,
|
|
65
|
+
initial=old_position,
|
|
66
|
+
target=value,
|
|
67
|
+
name=self.name,
|
|
68
|
+
)
|
|
69
|
+
except RuntimeError as exc:
|
|
70
|
+
self._set_success = False
|
|
71
|
+
raise RuntimeError("Motor was stopped") from exc
|
|
72
|
+
if not self._set_success:
|
|
73
|
+
raise RuntimeError("Motor was stopped")
|
|
74
|
+
|
|
75
|
+
def stop(self, success: bool = True) -> AsyncStatus:
|
|
76
|
+
self._set_success = success
|
|
77
|
+
return self._stop.trigger()
|