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.
Files changed (116) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +17 -46
  3. ophyd_async/core/_detector.py +68 -44
  4. ophyd_async/core/_device.py +120 -79
  5. ophyd_async/core/_device_filler.py +17 -8
  6. ophyd_async/core/_flyer.py +2 -2
  7. ophyd_async/core/_protocol.py +0 -28
  8. ophyd_async/core/_readable.py +30 -23
  9. ophyd_async/core/_settings.py +104 -0
  10. ophyd_async/core/_signal.py +164 -151
  11. ophyd_async/core/_signal_backend.py +4 -1
  12. ophyd_async/core/_soft_signal_backend.py +2 -1
  13. ophyd_async/core/_table.py +27 -14
  14. ophyd_async/core/_utils.py +30 -5
  15. ophyd_async/core/_yaml_settings.py +64 -0
  16. ophyd_async/epics/adandor/__init__.py +9 -0
  17. ophyd_async/epics/adandor/_andor.py +45 -0
  18. ophyd_async/epics/adandor/_andor_controller.py +49 -0
  19. ophyd_async/epics/adandor/_andor_io.py +36 -0
  20. ophyd_async/epics/adaravis/__init__.py +3 -1
  21. ophyd_async/epics/adaravis/_aravis.py +23 -37
  22. ophyd_async/epics/adaravis/_aravis_controller.py +21 -30
  23. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  24. ophyd_async/epics/adcore/__init__.py +15 -8
  25. ophyd_async/epics/adcore/_core_detector.py +41 -0
  26. ophyd_async/epics/adcore/_core_io.py +56 -31
  27. ophyd_async/epics/adcore/_core_logic.py +99 -84
  28. ophyd_async/epics/adcore/_core_writer.py +219 -0
  29. ophyd_async/epics/adcore/_hdf_writer.py +33 -59
  30. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  31. ophyd_async/epics/adcore/_single_trigger.py +5 -4
  32. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  33. ophyd_async/epics/adcore/_utils.py +37 -36
  34. ophyd_async/epics/adkinetix/_kinetix.py +29 -24
  35. ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
  36. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  37. ophyd_async/epics/adpilatus/__init__.py +2 -2
  38. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  39. ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
  40. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  41. ophyd_async/epics/adsimdetector/__init__.py +3 -3
  42. ophyd_async/epics/adsimdetector/_sim.py +33 -17
  43. ophyd_async/epics/advimba/_vimba.py +23 -23
  44. ophyd_async/epics/advimba/_vimba_controller.py +21 -35
  45. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  46. ophyd_async/epics/core/_aioca.py +52 -21
  47. ophyd_async/epics/core/_p4p.py +59 -16
  48. ophyd_async/epics/core/_pvi_connector.py +4 -2
  49. ophyd_async/epics/core/_signal.py +9 -2
  50. ophyd_async/epics/core/_util.py +10 -1
  51. ophyd_async/epics/eiger/_eiger_controller.py +10 -5
  52. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  53. ophyd_async/epics/motor.py +26 -15
  54. ophyd_async/epics/sim/_ioc.py +29 -0
  55. ophyd_async/epics/{demo → sim}/_mover.py +12 -6
  56. ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
  57. ophyd_async/epics/testing/__init__.py +24 -0
  58. ophyd_async/epics/testing/_example_ioc.py +91 -0
  59. ophyd_async/epics/testing/_utils.py +50 -0
  60. ophyd_async/epics/testing/test_records.db +174 -0
  61. ophyd_async/epics/testing/test_records_pva.db +177 -0
  62. ophyd_async/fastcs/core.py +2 -2
  63. ophyd_async/fastcs/panda/__init__.py +0 -2
  64. ophyd_async/fastcs/panda/_block.py +9 -9
  65. ophyd_async/fastcs/panda/_control.py +9 -4
  66. ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
  67. ophyd_async/fastcs/panda/_table.py +4 -1
  68. ophyd_async/fastcs/panda/_trigger.py +7 -7
  69. ophyd_async/plan_stubs/__init__.py +14 -0
  70. ophyd_async/plan_stubs/_ensure_connected.py +11 -17
  71. ophyd_async/plan_stubs/_fly.py +2 -2
  72. ophyd_async/plan_stubs/_nd_attributes.py +7 -5
  73. ophyd_async/plan_stubs/_panda.py +13 -0
  74. ophyd_async/plan_stubs/_settings.py +125 -0
  75. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  76. ophyd_async/sim/__init__.py +19 -0
  77. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
  78. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
  79. ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
  80. ophyd_async/tango/__init__.py +0 -43
  81. ophyd_async/tango/{signal → core}/__init__.py +7 -2
  82. ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
  83. ophyd_async/tango/{signal → core}/_signal.py +16 -4
  84. ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
  85. ophyd_async/tango/{signal → core}/_tango_transport.py +13 -15
  86. ophyd_async/tango/{demo → sim}/_counter.py +6 -7
  87. ophyd_async/tango/{demo → sim}/_mover.py +13 -9
  88. ophyd_async/testing/__init__.py +52 -0
  89. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  90. ophyd_async/testing/_assert.py +176 -0
  91. ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
  92. ophyd_async/testing/_one_of_everything.py +126 -0
  93. ophyd_async/testing/_wait_for_pending.py +22 -0
  94. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +50 -48
  95. ophyd_async-0.9.0.dist-info/RECORD +129 -0
  96. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/WHEEL +1 -1
  97. ophyd_async/core/_device_save_loader.py +0 -274
  98. ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
  99. ophyd_async/fastcs/panda/_utils.py +0 -16
  100. ophyd_async/sim/demo/__init__.py +0 -19
  101. ophyd_async/sim/testing/__init__.py +0 -0
  102. ophyd_async/tango/base_devices/__init__.py +0 -4
  103. ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
  104. ophyd_async-0.8.0a5.dist-info/entry_points.txt +0 -2
  105. /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
  106. /ophyd_async/epics/{demo → sim}/mover.db +0 -0
  107. /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
  108. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
  109. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
  110. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
  111. /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
  112. /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
  113. /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
  114. /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
  115. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
  116. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Callable, Mapping
5
+ from typing import Any
6
+
7
+ import bluesky.plan_stubs as bps
8
+ import numpy as np
9
+ from bluesky.utils import MsgGenerator, plan
10
+
11
+ from ophyd_async.core import (
12
+ Device,
13
+ Settings,
14
+ SettingsProvider,
15
+ SignalRW,
16
+ T,
17
+ walk_rw_signals,
18
+ )
19
+ from ophyd_async.core._table import Table
20
+
21
+ from ._wait_for_awaitable import wait_for_awaitable
22
+
23
+
24
+ @plan
25
+ def _get_values_of_signals(
26
+ signals: Mapping[T, SignalRW],
27
+ ) -> MsgGenerator[dict[T, Any]]:
28
+ coros = [sig.get_value() for sig in signals.values()]
29
+ values = yield from wait_for_awaitable(asyncio.gather(*coros))
30
+ named_values = dict(zip(signals, values, strict=True))
31
+ return named_values
32
+
33
+
34
+ @plan
35
+ def get_current_settings(device: Device) -> MsgGenerator[Settings]:
36
+ signals = walk_rw_signals(device)
37
+ named_values = yield from _get_values_of_signals(signals)
38
+ signal_values = {signals[name]: value for name, value in named_values.items()}
39
+ return Settings(device, signal_values)
40
+
41
+
42
+ @plan
43
+ def store_settings(
44
+ provider: SettingsProvider, name: str, device: Device
45
+ ) -> MsgGenerator[None]:
46
+ """Walk a Device for SignalRWs and store their values with a provider associated
47
+ with the given name.
48
+ """
49
+ signals = walk_rw_signals(device)
50
+ named_values = yield from _get_values_of_signals(signals)
51
+ yield from wait_for_awaitable(provider.store(name, named_values))
52
+
53
+
54
+ @plan
55
+ def retrieve_settings(
56
+ provider: SettingsProvider, name: str, device: Device
57
+ ) -> MsgGenerator[Settings]:
58
+ """Retrieve named Settings for a Device from a provider."""
59
+ named_values = yield from wait_for_awaitable(provider.retrieve(name))
60
+ signals = walk_rw_signals(device)
61
+ unknown_names = set(named_values) - set(signals)
62
+ if unknown_names:
63
+ raise NameError(f"Unknown signal names {sorted(unknown_names)}")
64
+ signal_values = {signals[name]: value for name, value in named_values.items()}
65
+ return Settings(device, signal_values)
66
+
67
+
68
+ @plan
69
+ def apply_settings(settings: Settings) -> MsgGenerator[None]:
70
+ """Set every SignalRW to the given value in Settings. If value is None ignore it."""
71
+ signal_values = {
72
+ signal: value for signal, value in settings.items() if value is not None
73
+ }
74
+ if signal_values:
75
+ for signal, value in signal_values.items():
76
+ yield from bps.abs_set(signal, value, group="apply_settings")
77
+ yield from bps.wait("apply_settings")
78
+
79
+
80
+ @plan
81
+ def apply_settings_if_different(
82
+ settings: Settings,
83
+ apply_plan: Callable[[Settings], MsgGenerator[None]],
84
+ current_settings: Settings | None = None,
85
+ ) -> MsgGenerator[None]:
86
+ """Set every SignalRW in settings to its given value if it is different to the
87
+ current value.
88
+
89
+ Parameters
90
+ ----------
91
+ apply_plan:
92
+ A device specific plan which takes the Settings to apply and applies them to
93
+ the Device. Used to add device specific ordering to setting the signals.
94
+ current_settings:
95
+ If given, should be a superset of settings containing the current value of
96
+ the Settings in the Device. If not given it will be created by reading just
97
+ the signals given in settings.
98
+ """
99
+ if current_settings is None:
100
+ # If we aren't give the current settings, then get the
101
+ # values of just the signals we were asked to change.
102
+ # This allows us to use this plan with Settings for a subset
103
+ # of signals in the Device without retrieving them all
104
+ signal_values = yield from _get_values_of_signals(
105
+ {sig: sig for sig in settings}
106
+ )
107
+ current_settings = Settings(settings.device, signal_values)
108
+
109
+ def _is_different(current, required) -> bool:
110
+ if isinstance(current, Table):
111
+ current = current.model_dump()
112
+ if isinstance(required, Table):
113
+ required = required.model_dump()
114
+ return current.keys() != required.keys() or any(
115
+ _is_different(current[k], required[k]) for k in current
116
+ )
117
+ elif isinstance(current, np.ndarray):
118
+ return not np.array_equal(current, required)
119
+ else:
120
+ return current != required
121
+
122
+ settings_to_change, _ = settings.partition(
123
+ lambda sig: _is_different(current_settings[sig], settings[sig])
124
+ )
125
+ yield from apply_plan(settings_to_change)
@@ -0,0 +1,13 @@
1
+ from collections.abc import Awaitable
2
+
3
+ import bluesky.plan_stubs as bps
4
+ from bluesky.utils import MsgGenerator, plan
5
+
6
+ from ophyd_async.core import T
7
+
8
+
9
+ @plan
10
+ def wait_for_awaitable(coro: Awaitable[T]) -> MsgGenerator[T]:
11
+ """Wait for a single awaitable to complete, and return the result."""
12
+ (task,) = yield from bps.wait_for([lambda: coro])
13
+ return task.result()
@@ -0,0 +1,19 @@
1
+ from ._pattern_detector import (
2
+ DATA_PATH,
3
+ SUM_PATH,
4
+ PatternDetector,
5
+ PatternDetectorController,
6
+ PatternDetectorWriter,
7
+ PatternGenerator,
8
+ )
9
+ from ._sim_motor import SimMotor
10
+
11
+ __all__ = [
12
+ "DATA_PATH",
13
+ "SUM_PATH",
14
+ "PatternGenerator",
15
+ "PatternDetector",
16
+ "PatternDetectorController",
17
+ "PatternDetectorWriter",
18
+ "SimMotor",
19
+ ]
@@ -27,8 +27,15 @@ class PatternDetectorController(DetectorController):
27
27
  )
28
28
 
29
29
  async def arm(self):
30
- assert self._trigger_info.livetime
31
- assert self.period
30
+ if not hasattr(self, "_trigger_info"):
31
+ msg = "TriggerInfo information is missing, has 'prepare' been called?"
32
+ raise RuntimeError(msg)
33
+ if not self._trigger_info.livetime:
34
+ msg = "Livetime information is missing in trigger info"
35
+ raise ValueError(msg)
36
+ if not self.period:
37
+ msg = "Period is not set"
38
+ raise ValueError(msg)
32
39
  self.task = asyncio.create_task(
33
40
  self._coroutine_for_image_writing(
34
41
  self._trigger_info.livetime,
@@ -67,11 +67,14 @@ class PatternGenerator:
67
67
 
68
68
  def write_data_to_dataset(self, path: str, data_shape: tuple[int, ...], data):
69
69
  """Write data to named dataset, resizing to fit and flushing after."""
70
- assert self._handle_for_h5_file, "no file has been opened!"
70
+ if not self._handle_for_h5_file:
71
+ msg = "No file has been opened!"
72
+ raise OSError(msg)
73
+
71
74
  dset = self._handle_for_h5_file[path]
72
- assert isinstance(
73
- dset, h5py.Dataset
74
- ), f"Expected {path} to be dataset, got {dset}"
75
+ if not isinstance(dset, h5py.Dataset):
76
+ msg = f"Expected {path} to be a dataset, got {type(dset).__name__}"
77
+ raise TypeError(msg)
75
78
  dset.resize((self.image_counter + 1,) + data_shape)
76
79
  dset[self.image_counter] = data
77
80
  dset.flush()
@@ -114,7 +117,9 @@ class PatternGenerator:
114
117
 
115
118
  self._handle_for_h5_file = h5py.File(self.target_path, "w", libver="latest")
116
119
 
117
- assert self._handle_for_h5_file, "not loaded the file right"
120
+ if not self._handle_for_h5_file:
121
+ msg = f"Problem opening file {self.target_path}"
122
+ raise OSError(msg)
118
123
 
119
124
  self._handle_for_h5_file.create_dataset(
120
125
  name=DATA_PATH,
@@ -184,7 +189,9 @@ class PatternGenerator:
184
189
  # cannot get the full filename the HDF writer will write
185
190
  # until the first frame comes in
186
191
  if not self._hdf_stream_provider:
187
- assert self.target_path, "open file has not been called"
192
+ if self.target_path is None:
193
+ msg = "open file has not been called"
194
+ raise RuntimeError(msg)
188
195
  self._hdf_stream_provider = HDFFile(
189
196
  self.target_path,
190
197
  self._datasets,
@@ -2,6 +2,7 @@ import asyncio
2
2
  import contextlib
3
3
  import time
4
4
 
5
+ import numpy as np
5
6
  from bluesky.protocols import Movable, Stoppable
6
7
 
7
8
  from ophyd_async.core import (
@@ -44,22 +45,20 @@ class SimMotor(StandardReadable, Movable, Stoppable):
44
45
 
45
46
  async def _move(self, old_position: float, new_position: float, move_time: float):
46
47
  start = time.monotonic()
47
- distance = abs(new_position - old_position)
48
- while True:
49
- time_elapsed = round(time.monotonic() - start, 2)
50
-
51
- # update position based on time elapsed
52
- if time_elapsed >= move_time:
53
- # successfully reached our target position
54
- self._user_readback_set(new_position)
55
- break
56
- else:
57
- current_position = old_position + distance * time_elapsed / move_time
58
-
59
- self._user_readback_set(current_position)
60
-
61
- # 10hz update loop
62
- await asyncio.sleep(0.1)
48
+ # Make an array of relative update times at 10Hz intervals
49
+ update_times = np.arange(0.1, move_time, 0.1)
50
+ # With the end position appended
51
+ update_times = np.concatenate((update_times, [move_time]))
52
+ # Interpolate the [old, new] position array with those update times
53
+ new_positions = np.interp(
54
+ update_times, [0, move_time], [old_position, new_position]
55
+ )
56
+ for update_time, new_position in zip(update_times, new_positions, strict=True):
57
+ # Calculate how long to wait to get there
58
+ relative_time = time.monotonic() - start
59
+ await asyncio.sleep(update_time - relative_time)
60
+ # Update the readback position
61
+ self._user_readback_set(new_position)
63
62
 
64
63
  @WatchableAsyncStatus.wrap
65
64
  async def set(self, value: float):
@@ -75,22 +74,25 @@ class SimMotor(StandardReadable, Movable, Stoppable):
75
74
  self.velocity.get_value(),
76
75
  )
77
76
  # If zero velocity, do instant move
78
- move_time = abs(new_position - old_position) / velocity if velocity else 0
79
- self._move_status = AsyncStatus(
80
- self._move(old_position, new_position, move_time)
81
- )
82
- # If stop is called then this will raise a CancelledError, ignore it
83
- with contextlib.suppress(asyncio.CancelledError):
84
- async for current_position in observe_value(
85
- self.user_readback, done_status=self._move_status
86
- ):
87
- yield WatcherUpdate(
88
- current=current_position,
89
- initial=old_position,
90
- target=new_position,
91
- name=self.name,
92
- unit=units,
93
- )
77
+ if velocity == 0:
78
+ self._user_readback_set(new_position)
79
+ else:
80
+ move_time = abs(new_position - old_position) / velocity
81
+ self._move_status = AsyncStatus(
82
+ self._move(old_position, new_position, move_time)
83
+ )
84
+ # If stop is called then this will raise a CancelledError, ignore it
85
+ with contextlib.suppress(asyncio.CancelledError):
86
+ async for current_position in observe_value(
87
+ self.user_readback, done_status=self._move_status
88
+ ):
89
+ yield WatcherUpdate(
90
+ current=current_position,
91
+ initial=old_position,
92
+ target=new_position,
93
+ name=self.name,
94
+ unit=units,
95
+ )
94
96
  if not self._set_success:
95
97
  raise RuntimeError("Motor was stopped")
96
98
 
@@ -1,43 +0,0 @@
1
- from .base_devices import (
2
- TangoDevice,
3
- TangoReadable,
4
- tango_polling,
5
- )
6
- from .signal import (
7
- AttributeProxy,
8
- CommandProxy,
9
- TangoSignalBackend,
10
- ensure_proper_executor,
11
- get_dtype_extended,
12
- get_python_type,
13
- get_tango_trl,
14
- get_trl_descriptor,
15
- infer_python_type,
16
- infer_signal_type,
17
- make_backend,
18
- tango_signal_r,
19
- tango_signal_rw,
20
- tango_signal_w,
21
- tango_signal_x,
22
- )
23
-
24
- __all__ = [
25
- "TangoDevice",
26
- "TangoReadable",
27
- "tango_polling",
28
- "TangoSignalBackend",
29
- "get_python_type",
30
- "get_dtype_extended",
31
- "get_trl_descriptor",
32
- "get_tango_trl",
33
- "infer_python_type",
34
- "infer_signal_type",
35
- "make_backend",
36
- "AttributeProxy",
37
- "CommandProxy",
38
- "ensure_proper_executor",
39
- "tango_signal_r",
40
- "tango_signal_rw",
41
- "tango_signal_w",
42
- "tango_signal_x",
43
- ]
@@ -1,3 +1,4 @@
1
+ from ._base_device import TangoDevice, TangoPolling
1
2
  from ._signal import (
2
3
  infer_python_type,
3
4
  infer_signal_type,
@@ -7,6 +8,7 @@ from ._signal import (
7
8
  tango_signal_w,
8
9
  tango_signal_x,
9
10
  )
11
+ from ._tango_readable import TangoReadable
10
12
  from ._tango_transport import (
11
13
  AttributeProxy,
12
14
  CommandProxy,
@@ -18,7 +20,7 @@ from ._tango_transport import (
18
20
  get_trl_descriptor,
19
21
  )
20
22
 
21
- __all__ = (
23
+ __all__ = [
22
24
  "AttributeProxy",
23
25
  "CommandProxy",
24
26
  "ensure_proper_executor",
@@ -34,4 +36,7 @@ __all__ = (
34
36
  "tango_signal_rw",
35
37
  "tango_signal_w",
36
38
  "tango_signal_x",
37
- )
39
+ "TangoDevice",
40
+ "TangoReadable",
41
+ "TangoPolling",
42
+ ]
@@ -1,17 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TypeVar
4
-
5
- from ophyd_async.core import Device, DeviceConnector, DeviceFiller
6
- from ophyd_async.core._utils import LazyMock
7
- from ophyd_async.tango.signal import (
8
- TangoSignalBackend,
9
- infer_python_type,
10
- infer_signal_type,
11
- )
3
+ from dataclasses import dataclass
4
+ from typing import Any, Generic, TypeVar
5
+
6
+ from ophyd_async.core import Device, DeviceConnector, DeviceFiller, LazyMock
12
7
  from tango import DeviceProxy as DeviceProxy
13
8
  from tango.asyncio import DeviceProxy as AsyncDeviceProxy
14
9
 
10
+ from ._signal import TangoSignalBackend, infer_python_type, infer_signal_type
11
+
15
12
  T = TypeVar("T")
16
13
 
17
14
 
@@ -32,63 +29,45 @@ class TangoDevice(Device):
32
29
 
33
30
  trl: str = ""
34
31
  proxy: DeviceProxy | None = None
35
- _polling: tuple[bool, float, float | None, float | None] = (False, 0.1, None, 0.1)
36
- _signal_polling: dict[str, tuple[bool, float, float, float]] = {}
37
- _poll_only_annotated_signals: bool = True
38
32
 
39
33
  def __init__(
40
34
  self,
41
35
  trl: str | None = None,
42
36
  device_proxy: DeviceProxy | None = None,
37
+ support_events: bool = False,
43
38
  name: str = "",
44
39
  ) -> None:
45
40
  connector = TangoDeviceConnector(
46
- trl=trl,
47
- device_proxy=device_proxy,
48
- polling=self._polling,
49
- signal_polling=self._signal_polling,
41
+ trl=trl, device_proxy=device_proxy, support_events=support_events
50
42
  )
51
43
  super().__init__(name=name, connector=connector)
52
44
 
53
45
 
54
- def tango_polling(
55
- polling: tuple[float, float, float]
56
- | dict[str, tuple[float, float, float]]
57
- | None = None,
58
- signal_polling: dict[str, tuple[float, float, float]] | None = None,
59
- ):
60
- """
61
- Class decorator to configure polling for Tango devices.
62
-
63
- This decorator allows for the configuration of both device-level and signal-level
64
- polling for Tango devices. Polling is useful for device servers that do not support
65
- event-driven updates.
66
-
67
- Parameters
68
- ----------
69
- polling : Optional[Union[Tuple[float, float, float],
70
- Dict[str, Tuple[float, float, float]]]], optional
71
- Device-level polling configuration as a tuple of three floats representing the
72
- polling interval, polling timeout, and polling delay. Alternatively,
73
- a dictionary can be provided to specify signal-level polling configurations
74
- directly.
75
- signal_polling : Optional[Dict[str, Tuple[float, float, float]]], optional
76
- Signal-level polling configuration as a dictionary where keys are signal names
77
- and values are tuples of three floats representing the polling interval, polling
78
- timeout, and polling delay.
79
- """
80
- if isinstance(polling, dict):
81
- signal_polling = polling
82
- polling = None
46
+ @dataclass
47
+ class TangoPolling(Generic[T]):
48
+ ophyd_polling_period: float = 0.1
49
+ abs_change: T | None = None
50
+ rel_change: T | None = None
83
51
 
84
- def decorator(cls):
85
- if polling is not None:
86
- cls._polling = (True, *polling)
87
- if signal_polling is not None:
88
- cls._signal_polling = {k: (True, *v) for k, v in signal_polling.items()}
89
- return cls
90
52
 
91
- return decorator
53
+ def fill_backend_with_polling(
54
+ support_events: bool, backend: TangoSignalBackend, annotations: list[Any]
55
+ ):
56
+ unhandled = []
57
+ while annotations:
58
+ annotation = annotations.pop(0)
59
+ backend.allow_events(support_events)
60
+ if isinstance(annotation, TangoPolling):
61
+ backend.set_polling(
62
+ not support_events,
63
+ annotation.ophyd_polling_period,
64
+ annotation.abs_change,
65
+ annotation.rel_change,
66
+ )
67
+ else:
68
+ unhandled.append(annotation)
69
+ annotations.extend(unhandled)
70
+ # These leftover annotations will now be handled by the iterator
92
71
 
93
72
 
94
73
  class TangoDeviceConnector(DeviceConnector):
@@ -96,13 +75,11 @@ class TangoDeviceConnector(DeviceConnector):
96
75
  self,
97
76
  trl: str | None,
98
77
  device_proxy: DeviceProxy | None,
99
- polling: tuple[bool, float, float | None, float | None],
100
- signal_polling: dict[str, tuple[bool, float, float, float]],
78
+ support_events: bool,
101
79
  ) -> None:
102
80
  self.trl = trl
103
81
  self.proxy = device_proxy
104
- self._polling = polling
105
- self._signal_polling = signal_polling
82
+ self._support_events = support_events
106
83
 
107
84
  def create_children_from_annotations(self, device: Device):
108
85
  if not hasattr(self, "filler"):
@@ -110,11 +87,14 @@ class TangoDeviceConnector(DeviceConnector):
110
87
  device=device,
111
88
  signal_backend_factory=TangoSignalBackend,
112
89
  device_connector_factory=lambda: TangoDeviceConnector(
113
- None, None, (False, 0.1, None, None), {}
90
+ None, None, self._support_events
114
91
  ),
115
92
  )
116
93
  list(self.filler.create_devices_from_annotations(filled=False))
117
- list(self.filler.create_signals_from_annotations(filled=False))
94
+ for backend, annotations in self.filler.create_signals_from_annotations(
95
+ filled=False
96
+ ):
97
+ fill_backend_with_polling(self._support_events, backend, annotations)
118
98
  self.filler.check_created()
119
99
 
120
100
  async def connect_mock(self, device: Device, mock: LazyMock):
@@ -145,12 +125,6 @@ class TangoDeviceConnector(DeviceConnector):
145
125
  backend = self.filler.fill_child_signal(name, signal_type)
146
126
  backend.datatype = await infer_python_type(full_trl, self.proxy)
147
127
  backend.set_trl(full_trl)
148
- if polling := self._signal_polling.get(name, ()):
149
- backend.set_polling(*polling)
150
- backend.allow_events(False)
151
- elif self._polling[0]:
152
- backend.set_polling(*self._polling)
153
- backend.allow_events(False)
154
128
  # Check that all the requested children have been filled
155
129
  self.filler.check_filled(f"{self.trl}: {children}")
156
130
  # Set the name of the device to name all children
@@ -16,11 +16,20 @@ from ophyd_async.core import (
16
16
  SignalW,
17
17
  SignalX,
18
18
  )
19
- from tango import AttrDataFormat, AttrWriteType, CmdArgType, DeviceProxy, DevState
19
+ from tango import (
20
+ AttrDataFormat,
21
+ AttrWriteType,
22
+ CmdArgType,
23
+ DeviceProxy,
24
+ DevState,
25
+ NonSupportedFeature, # type: ignore
26
+ )
20
27
  from tango.asyncio import DeviceProxy as AsyncDeviceProxy
21
28
 
22
29
  from ._tango_transport import TangoSignalBackend, get_python_type
23
30
 
31
+ logger = logging.getLogger("ophyd_async")
32
+
24
33
 
25
34
  def make_backend(
26
35
  datatype: type[SignalDatatypeT] | None,
@@ -174,8 +183,11 @@ async def infer_signal_type(
174
183
  else:
175
184
  dev_proxy = proxy
176
185
 
177
- if tr_name in dev_proxy.get_pipe_list():
178
- raise NotImplementedError("Pipes are not supported")
186
+ try:
187
+ if tr_name in dev_proxy.get_pipe_list():
188
+ raise NotImplementedError("Pipes are not supported")
189
+ except NonSupportedFeature: # type: ignore
190
+ pass
179
191
 
180
192
  if tr_name not in dev_proxy.get_attribute_list():
181
193
  if tr_name not in dev_proxy.get_command_list():
@@ -195,7 +207,7 @@ async def infer_signal_type(
195
207
  if config.in_type == CmdArgType.DevVoid:
196
208
  return SignalX
197
209
  elif config.in_type != config.out_type:
198
- logging.debug("Commands with different in and out dtypes are not supported")
210
+ logger.debug("Commands with different in and out dtypes are not supported")
199
211
  return None
200
212
  else:
201
213
  return SignalRW
@@ -1,11 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- from ophyd_async.core import (
4
- StandardReadable,
5
- )
6
- from ophyd_async.tango.base_devices._base_device import TangoDevice
3
+ from ophyd_async.core import StandardReadable
7
4
  from tango import DeviceProxy
8
5
 
6
+ from ._base_device import TangoDevice
7
+
9
8
 
10
9
  class TangoReadable(TangoDevice, StandardReadable):
11
10
  """
@@ -189,7 +189,7 @@ class AttributeProxy(TangoProxy):
189
189
  raise TimeoutError(f"{self._name} attr put failed: Timeout") from te
190
190
  except DevFailed as de:
191
191
  raise RuntimeError(
192
- f"{self._name} device" f" failure: {de.args[0].desc}"
192
+ f"{self._name} device failure: {de.args[0].desc}"
193
193
  ) from de
194
194
 
195
195
  else:
@@ -210,11 +210,11 @@ class AttributeProxy(TangoProxy):
210
210
  await asyncio.sleep(A_BIT)
211
211
  if to and (time.time() - start_time > to):
212
212
  raise TimeoutError(
213
- f"{self._name} attr put failed:" f" Timeout"
213
+ f"{self._name} attr put failed: Timeout"
214
214
  ) from exc
215
215
  else:
216
216
  raise RuntimeError(
217
- f"{self._name} device failure:" f" {exc.args[0].desc}"
217
+ f"{self._name} device failure: {exc.args[0].desc}"
218
218
  ) from exc
219
219
 
220
220
  return AsyncStatus(wait_for_reply(rid, timeout))
@@ -422,7 +422,7 @@ class CommandProxy(TangoProxy):
422
422
  raise TimeoutError(f"{self._name} command failed: Timeout") from te
423
423
  except DevFailed as de:
424
424
  raise RuntimeError(
425
- f"{self._name} device" f" failure: {de.args[0].desc}"
425
+ f"{self._name} device failure: {de.args[0].desc}"
426
426
  ) from de
427
427
 
428
428
  else:
@@ -446,8 +446,7 @@ class CommandProxy(TangoProxy):
446
446
  ) from de_exc
447
447
  else:
448
448
  raise RuntimeError(
449
- f"{self._name} device failure:"
450
- f" {de_exc.args[0].desc}"
449
+ f"{self._name} device failure: {de_exc.args[0].desc}"
451
450
  ) from de_exc
452
451
 
453
452
  return AsyncStatus(wait_for_reply(rid, timeout))
@@ -733,22 +732,21 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
733
732
  " for which polling is disabled."
734
733
  )
735
734
 
735
+ if callback and self.proxies[self.read_trl].has_subscription(): # type: ignore
736
+ msg = "Cannot set a callback when one is already set"
737
+ raise RuntimeError(msg)
738
+
739
+ if self.proxies[self.read_trl].has_subscription(): # type: ignore
740
+ self.proxies[self.read_trl].unsubscribe_callback() # type: ignore
741
+
736
742
  if callback:
737
743
  try:
738
- assert not self.proxies[self.read_trl].has_subscription() # type: ignore
739
744
  self.proxies[self.read_trl].subscribe_callback(callback) # type: ignore
740
- except AssertionError as ae:
741
- raise RuntimeError(
742
- "Cannot set a callback when one" " is already set"
743
- ) from ae
744
745
  except RuntimeError as exc:
745
746
  raise RuntimeError(
746
- f"Cannot set callback" f" for {self.read_trl}. {exc}"
747
+ f"Cannot set callback for {self.read_trl}. {exc}"
747
748
  ) from exc
748
749
 
749
- else:
750
- self.proxies[self.read_trl].unsubscribe_callback() # type: ignore
751
-
752
750
  def set_polling(
753
751
  self,
754
752
  allow_polling: bool = True,