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.
Files changed (40) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +4 -4
  3. ophyd_async/core/_detector.py +74 -37
  4. ophyd_async/core/_device.py +6 -1
  5. ophyd_async/core/_flyer.py +5 -20
  6. ophyd_async/core/_table.py +101 -18
  7. ophyd_async/epics/adaravis/_aravis_controller.py +4 -4
  8. ophyd_async/epics/adcore/_core_logic.py +2 -2
  9. ophyd_async/epics/adkinetix/_kinetix_controller.py +3 -3
  10. ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -3
  11. ophyd_async/epics/adsimdetector/_sim.py +1 -1
  12. ophyd_async/epics/adsimdetector/_sim_controller.py +3 -3
  13. ophyd_async/epics/advimba/_vimba_controller.py +3 -3
  14. ophyd_async/epics/eiger/_eiger_controller.py +3 -3
  15. ophyd_async/fastcs/panda/_block.py +7 -0
  16. ophyd_async/fastcs/panda/_control.py +2 -2
  17. ophyd_async/fastcs/panda/_table.py +3 -37
  18. ophyd_async/fastcs/panda/_trigger.py +3 -3
  19. ophyd_async/fastcs/panda/_writer.py +2 -2
  20. ophyd_async/plan_stubs/_fly.py +1 -3
  21. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +5 -3
  22. ophyd_async/tango/__init__.py +45 -0
  23. ophyd_async/tango/base_devices/__init__.py +4 -0
  24. ophyd_async/tango/base_devices/_base_device.py +225 -0
  25. ophyd_async/tango/base_devices/_tango_readable.py +33 -0
  26. ophyd_async/tango/demo/__init__.py +12 -0
  27. ophyd_async/tango/demo/_counter.py +37 -0
  28. ophyd_async/tango/demo/_detector.py +42 -0
  29. ophyd_async/tango/demo/_mover.py +77 -0
  30. ophyd_async/tango/demo/_tango/__init__.py +3 -0
  31. ophyd_async/tango/demo/_tango/_servers.py +108 -0
  32. ophyd_async/tango/signal/__init__.py +39 -0
  33. ophyd_async/tango/signal/_signal.py +223 -0
  34. ophyd_async/tango/signal/_tango_transport.py +764 -0
  35. {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/METADATA +5 -1
  36. {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/RECORD +40 -28
  37. {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/WHEEL +1 -1
  38. {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/LICENSE +0 -0
  39. {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/entry_points.txt +0 -0
  40. {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
- DetectorControl,
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(DetectorControl):
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, field_validator, model_validator
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
- if isinstance(trigger, SeqTrigger):
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 TriggerLogic, wait_for_value
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(TriggerLogic[SeqTableInfo]):
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(TriggerLogic[PcompInfo]):
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.num_capture.set(0),
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.
@@ -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
- number=number_of_frames * repeats,
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 DetectorControl, PathProvider
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(DetectorControl):
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, self.period, self._trigger_info.number
35
+ self._trigger_info.livetime,
36
+ self.period,
37
+ self._trigger_info.total_number_of_triggers,
36
38
  )
37
39
  )
38
40
 
@@ -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,4 @@
1
+ from ._base_device import TangoDevice, tango_polling
2
+ from ._tango_readable import TangoReadable
3
+
4
+ __all__ = ["TangoDevice", "TangoReadable", "tango_polling"]
@@ -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()
@@ -0,0 +1,3 @@
1
+ from ._servers import DemoCounter, DemoMover
2
+
3
+ __all__ = ["DemoCounter", "DemoMover"]