ophyd-async 0.5.2__py3-none-any.whl → 0.7.0a1__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 (79) hide show
  1. ophyd_async/__init__.py +10 -1
  2. ophyd_async/__main__.py +12 -4
  3. ophyd_async/_version.py +2 -2
  4. ophyd_async/core/__init__.py +15 -7
  5. ophyd_async/core/_detector.py +133 -87
  6. ophyd_async/core/_device.py +13 -15
  7. ophyd_async/core/_device_save_loader.py +30 -19
  8. ophyd_async/core/_flyer.py +6 -19
  9. ophyd_async/core/_hdf_dataset.py +8 -9
  10. ophyd_async/core/_log.py +3 -1
  11. ophyd_async/core/_mock_signal_backend.py +11 -9
  12. ophyd_async/core/_mock_signal_utils.py +8 -5
  13. ophyd_async/core/_protocol.py +7 -7
  14. ophyd_async/core/_providers.py +11 -11
  15. ophyd_async/core/_readable.py +30 -22
  16. ophyd_async/core/_signal.py +52 -51
  17. ophyd_async/core/_signal_backend.py +20 -7
  18. ophyd_async/core/_soft_signal_backend.py +62 -32
  19. ophyd_async/core/_status.py +7 -9
  20. ophyd_async/core/_table.py +146 -0
  21. ophyd_async/core/_utils.py +24 -28
  22. ophyd_async/epics/adaravis/_aravis_controller.py +20 -19
  23. ophyd_async/epics/adaravis/_aravis_io.py +2 -1
  24. ophyd_async/epics/adcore/_core_io.py +2 -0
  25. ophyd_async/epics/adcore/_core_logic.py +4 -5
  26. ophyd_async/epics/adcore/_hdf_writer.py +19 -8
  27. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  28. ophyd_async/epics/adcore/_utils.py +5 -6
  29. ophyd_async/epics/adkinetix/_kinetix_controller.py +20 -15
  30. ophyd_async/epics/adpilatus/_pilatus_controller.py +22 -18
  31. ophyd_async/epics/adsimdetector/_sim.py +7 -6
  32. ophyd_async/epics/adsimdetector/_sim_controller.py +22 -17
  33. ophyd_async/epics/advimba/_vimba_controller.py +22 -17
  34. ophyd_async/epics/demo/_mover.py +4 -5
  35. ophyd_async/epics/demo/sensor.db +0 -1
  36. ophyd_async/epics/eiger/_eiger.py +1 -1
  37. ophyd_async/epics/eiger/_eiger_controller.py +18 -18
  38. ophyd_async/epics/eiger/_odin_io.py +6 -5
  39. ophyd_async/epics/motor.py +8 -10
  40. ophyd_async/epics/pvi/_pvi.py +30 -33
  41. ophyd_async/epics/signal/_aioca.py +55 -25
  42. ophyd_async/epics/signal/_common.py +3 -10
  43. ophyd_async/epics/signal/_epics_transport.py +11 -8
  44. ophyd_async/epics/signal/_p4p.py +79 -30
  45. ophyd_async/epics/signal/_signal.py +6 -8
  46. ophyd_async/fastcs/panda/__init__.py +0 -6
  47. ophyd_async/fastcs/panda/_control.py +16 -17
  48. ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
  49. ophyd_async/fastcs/panda/_table.py +77 -138
  50. ophyd_async/fastcs/panda/_trigger.py +4 -5
  51. ophyd_async/fastcs/panda/_utils.py +3 -2
  52. ophyd_async/fastcs/panda/_writer.py +28 -13
  53. ophyd_async/plan_stubs/_fly.py +15 -17
  54. ophyd_async/plan_stubs/_nd_attributes.py +12 -6
  55. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
  56. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +27 -21
  57. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
  58. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
  59. ophyd_async/sim/demo/_sim_motor.py +2 -1
  60. ophyd_async/tango/__init__.py +45 -0
  61. ophyd_async/tango/base_devices/__init__.py +4 -0
  62. ophyd_async/tango/base_devices/_base_device.py +225 -0
  63. ophyd_async/tango/base_devices/_tango_readable.py +33 -0
  64. ophyd_async/tango/demo/__init__.py +12 -0
  65. ophyd_async/tango/demo/_counter.py +37 -0
  66. ophyd_async/tango/demo/_detector.py +42 -0
  67. ophyd_async/tango/demo/_mover.py +77 -0
  68. ophyd_async/tango/demo/_tango/__init__.py +3 -0
  69. ophyd_async/tango/demo/_tango/_servers.py +108 -0
  70. ophyd_async/tango/signal/__init__.py +39 -0
  71. ophyd_async/tango/signal/_signal.py +223 -0
  72. ophyd_async/tango/signal/_tango_transport.py +764 -0
  73. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/METADATA +50 -45
  74. ophyd_async-0.7.0a1.dist-info/RECORD +108 -0
  75. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/WHEEL +1 -1
  76. ophyd_async-0.5.2.dist-info/RECORD +0 -95
  77. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/LICENSE +0 -0
  78. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/entry_points.txt +0 -0
  79. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
- from ophyd_async.core import AsyncStatus, DetectorControl, DetectorTrigger, PathProvider
3
+ from ophyd_async.core import DetectorController, PathProvider
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,
@@ -14,36 +14,42 @@ class PatternDetectorController(DetectorControl):
14
14
  exposure: float = 0.1,
15
15
  ) -> None:
16
16
  self.pattern_generator: PatternGenerator = pattern_generator
17
- if exposure is None:
18
- exposure = 0.1
19
17
  self.pattern_generator.set_exposure(exposure)
20
18
  self.path_provider: PathProvider = path_provider
21
- self.task: Optional[asyncio.Task] = None
19
+ self.task: asyncio.Task | None = None
22
20
  super().__init__()
23
21
 
24
- async def arm(
25
- self,
26
- num: int,
27
- trigger: DetectorTrigger = DetectorTrigger.internal,
28
- exposure: Optional[float] = 0.01,
29
- ) -> AsyncStatus:
30
- if exposure is None:
31
- exposure = 0.1
32
- period: float = exposure + self.get_deadtime(exposure)
33
- task = asyncio.create_task(
34
- self._coroutine_for_image_writing(exposure, period, num)
22
+ async def prepare(self, trigger_info: TriggerInfo):
23
+ self._trigger_info = trigger_info
24
+ if self._trigger_info.livetime is None:
25
+ self._trigger_info.livetime = 0.01
26
+ self.period: float = self._trigger_info.livetime + self.get_deadtime(
27
+ trigger_info.livetime
35
28
  )
36
- self.task = task
37
- return AsyncStatus(task)
38
29
 
39
- async def disarm(self):
30
+ async def arm(self):
31
+ assert self._trigger_info.livetime
32
+ assert self.period
33
+ self.task = asyncio.create_task(
34
+ self._coroutine_for_image_writing(
35
+ self._trigger_info.livetime,
36
+ self.period,
37
+ self._trigger_info.total_number_of_triggers,
38
+ )
39
+ )
40
+
41
+ async def wait_for_idle(self):
40
42
  if self.task:
43
+ await self.task
44
+
45
+ async def disarm(self):
46
+ if self.task and not self.task.done():
41
47
  self.task.cancel()
42
48
  try:
43
49
  await self.task
44
50
  except asyncio.CancelledError:
45
51
  pass
46
- self.task = None
52
+ self.task = None
47
53
 
48
54
  def get_deadtime(self, exposure: float | None) -> float:
49
55
  return 0.001
@@ -1,8 +1,8 @@
1
- from typing import AsyncGenerator, AsyncIterator, Dict
1
+ from collections.abc import AsyncGenerator, AsyncIterator
2
2
 
3
- from bluesky.protocols import DataKey
3
+ from event_model import DataKey
4
4
 
5
- from ophyd_async.core import DetectorWriter, NameProvider, PathProvider
5
+ from ophyd_async.core import DEFAULT_TIMEOUT, DetectorWriter, NameProvider, PathProvider
6
6
 
7
7
  from ._pattern_generator import PatternGenerator
8
8
 
@@ -20,7 +20,7 @@ class PatternDetectorWriter(DetectorWriter):
20
20
  self.path_provider = path_provider
21
21
  self.name_provider = name_provider
22
22
 
23
- async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
23
+ async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
24
24
  return await self.pattern_generator.open_file(
25
25
  self.path_provider, self.name_provider(), multiplier
26
26
  )
@@ -31,8 +31,11 @@ class PatternDetectorWriter(DetectorWriter):
31
31
  def collect_stream_docs(self, indices_written: int) -> AsyncIterator:
32
32
  return self.pattern_generator.collect_stream_docs(indices_written)
33
33
 
34
- def observe_indices_written(self, timeout=...) -> AsyncGenerator[int, None]:
35
- return self.pattern_generator.observe_indices_written()
34
+ async def observe_indices_written(
35
+ self, timeout=DEFAULT_TIMEOUT
36
+ ) -> AsyncGenerator[int, None]:
37
+ async for index in self.pattern_generator.observe_indices_written(timeout):
38
+ yield index
36
39
 
37
40
  async def get_indices_written(self) -> int:
38
41
  return self.pattern_generator.image_counter
@@ -1,9 +1,10 @@
1
+ from collections.abc import AsyncGenerator, AsyncIterator
1
2
  from pathlib import Path
2
- from typing import AsyncGenerator, AsyncIterator, Dict, Optional
3
3
 
4
4
  import h5py
5
5
  import numpy as np
6
- from bluesky.protocols import DataKey, StreamAsset
6
+ from bluesky.protocols import StreamAsset
7
+ from event_model import DataKey
7
8
 
8
9
  from ophyd_async.core import (
9
10
  DEFAULT_TIMEOUT,
@@ -60,19 +61,22 @@ class PatternGenerator:
60
61
  generate_gaussian_blob(width=detector_width, height=detector_height)
61
62
  * MAX_UINT8_VALUE
62
63
  )
63
- self._hdf_stream_provider: Optional[HDFFile] = None
64
- self._handle_for_h5_file: Optional[h5py.File] = None
65
- self.target_path: Optional[Path] = None
64
+ self._hdf_stream_provider: HDFFile | None = None
65
+ self._handle_for_h5_file: h5py.File | None = None
66
+ self.target_path: Path | None = None
66
67
 
67
- async def write_image_to_file(self) -> None:
68
+ def write_data_to_dataset(self, path: str, data_shape: tuple[int, ...], data):
69
+ """Write data to named dataset, resizing to fit and flushing after."""
68
70
  assert self._handle_for_h5_file, "no file has been opened!"
69
- # prepare - resize the fixed hdf5 data structure
70
- # so that the new image can be written
71
- self._handle_for_h5_file[DATA_PATH].resize(
72
- (self.image_counter + 1, self.height, self.width)
73
- )
74
- self._handle_for_h5_file[SUM_PATH].resize((self.image_counter + 1,))
71
+ dset = self._handle_for_h5_file[path]
72
+ assert isinstance(
73
+ dset, h5py.Dataset
74
+ ), f"Expected {path} to be dataset, got {dset}"
75
+ dset.resize((self.image_counter + 1,) + data_shape)
76
+ dset[self.image_counter] = data
77
+ dset.flush()
75
78
 
79
+ async def write_image_to_file(self) -> None:
76
80
  # generate the simulated data
77
81
  intensity: float = generate_interesting_pattern(self.x, self.y)
78
82
  detector_data = (
@@ -82,14 +86,9 @@ class PatternGenerator:
82
86
  / self.saturation_exposure_time
83
87
  ).astype(np.uint8)
84
88
 
85
- # write data to disc (intermediate step)
86
- self._handle_for_h5_file[DATA_PATH][self.image_counter] = detector_data
87
- sum = np.sum(detector_data)
88
- self._handle_for_h5_file[SUM_PATH][self.image_counter] = sum
89
-
90
- # save metadata - so that it's discoverable
91
- self._handle_for_h5_file[DATA_PATH].flush()
92
- self._handle_for_h5_file[SUM_PATH].flush()
89
+ # Write the data and sum
90
+ self.write_data_to_dataset(DATA_PATH, (self.height, self.width), detector_data)
91
+ self.write_data_to_dataset(SUM_PATH, (), np.sum(detector_data))
93
92
 
94
93
  # counter increment is last
95
94
  # as only at this point the new data is visible from the outside
@@ -107,7 +106,7 @@ class PatternGenerator:
107
106
 
108
107
  async def open_file(
109
108
  self, path_provider: PathProvider, name: str, multiplier: int = 1
110
- ) -> Dict[str, DataKey]:
109
+ ) -> dict[str, DataKey]:
111
110
  await self.counter_signal.connect()
112
111
 
113
112
  self.target_path = self._get_new_path(path_provider)
@@ -156,7 +155,7 @@ class PatternGenerator:
156
155
  describe = {
157
156
  ds.data_key: DataKey(
158
157
  source="sim://pattern-generator-hdf-file",
159
- shape=outer_shape + tuple(ds.shape),
158
+ shape=list(outer_shape) + list(ds.shape),
160
159
  dtype="array" if ds.shape else "number",
161
160
  external="STREAM:",
162
161
  )
@@ -199,7 +198,6 @@ class PatternGenerator:
199
198
  def close(self) -> None:
200
199
  if self._handle_for_h5_file:
201
200
  self._handle_for_h5_file.close()
202
- print("file closed")
203
201
  self._handle_for_h5_file = None
204
202
 
205
203
  async def observe_indices_written(
@@ -63,10 +63,11 @@ class SimMotor(StandardReadable, Movable, Stoppable):
63
63
  await asyncio.sleep(0.1)
64
64
 
65
65
  @WatchableAsyncStatus.wrap
66
- async def set(self, new_position: float):
66
+ async def set(self, value: float):
67
67
  """
68
68
  Asynchronously move the motor to a new position.
69
69
  """
70
+ new_position = value
70
71
  # Make sure any existing move tasks are stopped
71
72
  await self.stop()
72
73
  old_position, units, velocity = await asyncio.gather(
@@ -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)