ophyd-async 0.8.0a5__py3-none-any.whl → 0.9.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 (64) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +4 -26
  3. ophyd_async/core/_detector.py +9 -9
  4. ophyd_async/core/_device.py +27 -8
  5. ophyd_async/core/_protocol.py +0 -28
  6. ophyd_async/core/_signal.py +111 -136
  7. ophyd_async/core/_table.py +9 -4
  8. ophyd_async/core/_utils.py +11 -2
  9. ophyd_async/epics/adaravis/_aravis_controller.py +8 -8
  10. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  11. ophyd_async/epics/adcore/_core_io.py +21 -21
  12. ophyd_async/epics/adcore/_core_logic.py +6 -3
  13. ophyd_async/epics/adcore/_hdf_writer.py +6 -3
  14. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  15. ophyd_async/epics/adcore/_utils.py +35 -35
  16. ophyd_async/epics/adkinetix/_kinetix_controller.py +7 -7
  17. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  18. ophyd_async/epics/adpilatus/_pilatus.py +3 -3
  19. ophyd_async/epics/adpilatus/_pilatus_controller.py +4 -4
  20. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  21. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  22. ophyd_async/epics/advimba/_vimba_controller.py +14 -14
  23. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  24. ophyd_async/epics/core/_p4p.py +19 -0
  25. ophyd_async/epics/core/_pvi_connector.py +4 -2
  26. ophyd_async/epics/core/_signal.py +9 -2
  27. ophyd_async/epics/core/_util.py +9 -0
  28. ophyd_async/epics/demo/_mover.py +2 -2
  29. ophyd_async/epics/demo/_sensor.py +2 -2
  30. ophyd_async/epics/eiger/_eiger_controller.py +10 -5
  31. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  32. ophyd_async/epics/motor.py +8 -5
  33. ophyd_async/epics/testing/__init__.py +24 -0
  34. ophyd_async/epics/testing/_example_ioc.py +107 -0
  35. ophyd_async/epics/testing/_utils.py +78 -0
  36. ophyd_async/epics/testing/test_records.db +158 -0
  37. ophyd_async/epics/testing/test_records_pva.db +177 -0
  38. ophyd_async/fastcs/core.py +2 -2
  39. ophyd_async/fastcs/panda/_block.py +9 -9
  40. ophyd_async/fastcs/panda/_control.py +2 -2
  41. ophyd_async/fastcs/panda/_hdf_panda.py +4 -1
  42. ophyd_async/fastcs/panda/_trigger.py +7 -7
  43. ophyd_async/plan_stubs/_fly.py +1 -1
  44. ophyd_async/sim/demo/_sim_motor.py +34 -32
  45. ophyd_async/tango/__init__.py +0 -43
  46. ophyd_async/tango/{signal → core}/__init__.py +7 -2
  47. ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
  48. ophyd_async/tango/{signal → core}/_signal.py +13 -3
  49. ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
  50. ophyd_async/tango/{signal → core}/_tango_transport.py +1 -1
  51. ophyd_async/tango/demo/_counter.py +6 -7
  52. ophyd_async/tango/demo/_mover.py +8 -7
  53. ophyd_async/testing/__init__.py +33 -0
  54. ophyd_async/testing/_assert.py +128 -0
  55. ophyd_async/{core → testing}/_mock_signal_utils.py +12 -8
  56. ophyd_async/testing/_wait_for_pending.py +22 -0
  57. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/METADATA +49 -47
  58. ophyd_async-0.9.0a1.dist-info/RECORD +119 -0
  59. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/WHEEL +1 -1
  60. ophyd_async/tango/base_devices/__init__.py +0 -4
  61. ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
  62. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/LICENSE +0 -0
  63. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/entry_points.txt +0 -0
  64. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,177 @@
1
+ record(waveform, "$(device)int8a") {
2
+ field(NELM, "3")
3
+ field(FTVL, "CHAR")
4
+ field(INP, {const:[-128, 127]})
5
+ field(PINI, "YES")
6
+ }
7
+
8
+ record(waveform, "$(device)uint16a") {
9
+ field(NELM, "3")
10
+ field(FTVL, "USHORT")
11
+ field(INP, {const:[0, 65535]})
12
+ field(PINI, "YES")
13
+ }
14
+
15
+ record(waveform, "$(device)uint32a") {
16
+ field(NELM, "3")
17
+ field(FTVL, "ULONG")
18
+ field(INP, {const:[0, 4294967295]})
19
+ field(PINI, "YES")
20
+ }
21
+
22
+ record(waveform, "$(device)int64a") {
23
+ field(NELM, "3")
24
+ field(FTVL, "INT64")
25
+ # Can't do 64-bit int with JSON numbers in a const link...
26
+ field(INP, {const:[-2147483649, 2147483648]})
27
+ field(PINI, "YES")
28
+ }
29
+
30
+ record(waveform, "$(device)uint64a") {
31
+ field(NELM, "3")
32
+ field(FTVL, "UINT64")
33
+ field(INP, {const:[0, 4294967297]})
34
+ field(PINI, "YES")
35
+ }
36
+
37
+ record(waveform, "$(device)table:labels") {
38
+ field(FTVL, "STRING")
39
+ field(NELM, "5")
40
+ field(INP, {const:["Bool", "Int", "Float", "Str", "Enum"]})
41
+ field(PINI, "YES")
42
+ info(Q:group, {
43
+ "$(device)table": {
44
+ "+id": "epics:nt/NTTable:1.0",
45
+ "labels": {
46
+ "+type": "plain",
47
+ "+channel": "VAL"
48
+ }
49
+ }
50
+ })
51
+ }
52
+
53
+ record(waveform, "$(device)table:bool")
54
+ {
55
+ field(FTVL, "UCHAR")
56
+ field(NELM, "4096")
57
+ field(INP, {const:[false, false, true, true]})
58
+ field(PINI, "YES")
59
+ info(Q:group, {
60
+ "$(device)table": {
61
+ "value.bool": {
62
+ "+type": "plain",
63
+ "+channel": "VAL",
64
+ "+putorder": 1
65
+ }
66
+ }
67
+ })
68
+ }
69
+
70
+ record(waveform, "$(device)table:int")
71
+ {
72
+ field(FTVL, "LONG")
73
+ field(NELM, "4096")
74
+ field(INP, {const:[1, 8, -9, 32]})
75
+ field(PINI, "YES")
76
+ info(Q:group, {
77
+ "$(device)table": {
78
+ "value.int": {
79
+ "+type": "plain",
80
+ "+channel": "VAL",
81
+ "+putorder": 2
82
+ }
83
+ }
84
+ })
85
+ }
86
+
87
+ record(waveform, "$(device)table:float")
88
+ {
89
+ field(FTVL, "DOUBLE")
90
+ field(NELM, "4096")
91
+ field(INP, {const:[1.8, 8.2, -6, 32.9887]})
92
+ field(PINI, "YES")
93
+ info(Q:group, {
94
+ "$(device)table": {
95
+ "value.float": {
96
+ "+type": "plain",
97
+ "+channel": "VAL",
98
+ "+putorder": 3
99
+ }
100
+ }
101
+ })
102
+ }
103
+
104
+ record(waveform, "$(device)table:str")
105
+ {
106
+ field(FTVL, "STRING")
107
+ field(NELM, "4096")
108
+ field(INP, {const:["Hello", "World", "Foo", "Bar"]})
109
+ field(PINI, "YES")
110
+ info(Q:group, {
111
+ "$(device)table": {
112
+ "value.str": {
113
+ "+type": "plain",
114
+ "+channel": "VAL",
115
+ "+putorder": 4
116
+ }
117
+ }
118
+ })
119
+ }
120
+
121
+ record(waveform, "$(device)table:enum")
122
+ {
123
+ field(FTVL, "STRING")
124
+ field(NELM, "4096")
125
+ field(INP, {const:["Aaa", "Bbb", "Aaa", "Ccc"]})
126
+ field(PINI, "YES")
127
+ info(Q:group, {
128
+ "$(device)table": {
129
+ "value.enum": {
130
+ "+type": "plain",
131
+ "+channel": "VAL",
132
+ "+putorder": 5,
133
+ "+trigger": "*",
134
+ },
135
+ "": {"+type": "meta", "+channel": "VAL"}
136
+ }
137
+ })
138
+ }
139
+
140
+ record(longout, "$(device)ntndarray:ArraySize0_RBV") {
141
+ field(VAL, "3")
142
+ field(PINI, "YES")
143
+ info(Q:group, {
144
+ "$(device)ntndarray":{
145
+ "dimension[0].size":{+channel:"VAL", +type:"plain", +putorder:0}
146
+ }
147
+ })
148
+ }
149
+
150
+ record(longout, "$(device)ntndarray:ArraySize1_RBV") {
151
+ field(VAL, "2")
152
+ field(PINI, "YES")
153
+ info(Q:group, {
154
+ "$(device)ntndarray":{
155
+ "dimension[1].size":{+channel:"VAL", +type:"plain", +putorder:0}
156
+ }
157
+ })
158
+ }
159
+
160
+ record(waveform, "$(device)ntndarray:data")
161
+ {
162
+ field(FTVL, "INT64")
163
+ field(NELM, "6")
164
+ field(INP, {const:[0, 0, 0, 0, 0, 0]})
165
+ field(PINI, "YES")
166
+ info(Q:group, {
167
+ "$(device)ntndarray":{
168
+ +id:"epics:nt/NTNDArray:1.0",
169
+ "value":{
170
+ +type:"any",
171
+ +channel:"VAL",
172
+ +trigger:"*",
173
+ },
174
+ "": {+type:"meta", +channel:"SEVR"}
175
+ }
176
+ })
177
+ }
@@ -2,8 +2,8 @@ from ophyd_async.core import Device, DeviceConnector
2
2
  from ophyd_async.epics.core import PviDeviceConnector
3
3
 
4
4
 
5
- def fastcs_connector(device: Device, uri: str) -> DeviceConnector:
5
+ def fastcs_connector(device: Device, uri: str, error_hint: str = "") -> DeviceConnector:
6
6
  # TODO: add Tango support based on uri scheme
7
- connector = PviDeviceConnector(uri)
7
+ connector = PviDeviceConnector(uri, error_hint)
8
8
  connector.create_children_from_annotations(device)
9
9
  return connector
@@ -36,14 +36,14 @@ class PulseBlock(Device):
36
36
 
37
37
 
38
38
  class PcompDirection(StrictEnum):
39
- positive = "Positive"
40
- negative = "Negative"
41
- either = "Either"
39
+ POSITIVE = "Positive"
40
+ NEGATIVE = "Negative"
41
+ EITHER = "Either"
42
42
 
43
43
 
44
44
  class BitMux(SubsetEnum):
45
- zero = "ZERO"
46
- one = "ONE"
45
+ ZERO = "ZERO"
46
+ ONE = "ONE"
47
47
 
48
48
 
49
49
  class PcompBlock(Device):
@@ -57,10 +57,10 @@ class PcompBlock(Device):
57
57
 
58
58
 
59
59
  class TimeUnits(StrictEnum):
60
- min = "min"
61
- s = "s"
62
- ms = "ms"
63
- us = "us"
60
+ MIN = "min"
61
+ S = "s"
62
+ MS = "ms"
63
+ US = "us"
64
64
 
65
65
 
66
66
  class SeqBlock(Device):
@@ -19,8 +19,8 @@ class PandaPcapController(DetectorController):
19
19
 
20
20
  async def prepare(self, trigger_info: TriggerInfo):
21
21
  assert trigger_info.trigger in (
22
- DetectorTrigger.constant_gate,
23
- DetectorTrigger.variable_gate,
22
+ DetectorTrigger.CONSTANT_GATE,
23
+ DetectorTrigger.VARIABLE_GATE,
24
24
  ), "Only constant_gate and variable_gate triggering is supported on the PandA"
25
25
 
26
26
  async def arm(self):
@@ -9,6 +9,8 @@ from ._block import CommonPandaBlocks
9
9
  from ._control import PandaPcapController
10
10
  from ._writer import PandaHDFWriter
11
11
 
12
+ MINIMUM_PANDA_IOC = "0.11.4"
13
+
12
14
 
13
15
  class HDFPanda(CommonPandaBlocks, StandardDetector):
14
16
  def __init__(
@@ -18,8 +20,9 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
18
20
  config_sigs: Sequence[SignalR] = (),
19
21
  name: str = "",
20
22
  ):
23
+ error_hint = f"Is PandABlocks-ioc at least version {MINIMUM_PANDA_IOC}?"
21
24
  # This has to be first so we make self.pcap
22
- connector = fastcs_connector(self, prefix)
25
+ connector = fastcs_connector(self, prefix, error_hint)
23
26
  controller = PandaPcapController(pcap=self.pcap)
24
27
  writer = PandaHDFWriter(
25
28
  path_provider=path_provider,
@@ -20,8 +20,8 @@ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
20
20
 
21
21
  async def prepare(self, value: SeqTableInfo):
22
22
  await asyncio.gather(
23
- self.seq.prescale_units.set(TimeUnits.us),
24
- self.seq.enable.set(BitMux.zero),
23
+ self.seq.prescale_units.set(TimeUnits.US),
24
+ self.seq.enable.set(BitMux.ZERO),
25
25
  )
26
26
  await asyncio.gather(
27
27
  self.seq.prescale.set(value.prescale_as_us),
@@ -30,14 +30,14 @@ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
30
30
  )
31
31
 
32
32
  async def kickoff(self) -> None:
33
- await self.seq.enable.set(BitMux.one)
33
+ await self.seq.enable.set(BitMux.ONE)
34
34
  await wait_for_value(self.seq.active, True, timeout=1)
35
35
 
36
36
  async def complete(self) -> None:
37
37
  await wait_for_value(self.seq.active, False, timeout=None)
38
38
 
39
39
  async def stop(self):
40
- await self.seq.enable.set(BitMux.zero)
40
+ await self.seq.enable.set(BitMux.ZERO)
41
41
  await wait_for_value(self.seq.active, False, timeout=1)
42
42
 
43
43
 
@@ -68,7 +68,7 @@ class StaticPcompTriggerLogic(FlyerController[PcompInfo]):
68
68
  self.pcomp = pcomp
69
69
 
70
70
  async def prepare(self, value: PcompInfo):
71
- await self.pcomp.enable.set(BitMux.zero)
71
+ await self.pcomp.enable.set(BitMux.ZERO)
72
72
  await asyncio.gather(
73
73
  self.pcomp.start.set(value.start_postion),
74
74
  self.pcomp.width.set(value.pulse_width),
@@ -78,12 +78,12 @@ class StaticPcompTriggerLogic(FlyerController[PcompInfo]):
78
78
  )
79
79
 
80
80
  async def kickoff(self) -> None:
81
- await self.pcomp.enable.set(BitMux.one)
81
+ await self.pcomp.enable.set(BitMux.ONE)
82
82
  await wait_for_value(self.pcomp.active, True, timeout=1)
83
83
 
84
84
  async def complete(self, timeout: float | None = None) -> None:
85
85
  await wait_for_value(self.pcomp.active, False, timeout=timeout)
86
86
 
87
87
  async def stop(self):
88
- await self.pcomp.enable.set(BitMux.zero)
88
+ await self.pcomp.enable.set(BitMux.ZERO)
89
89
  await wait_for_value(self.pcomp.active, False, timeout=1)
@@ -62,7 +62,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
62
62
 
63
63
  trigger_info = TriggerInfo(
64
64
  number_of_triggers=number_of_frames * repeats,
65
- trigger=DetectorTrigger.constant_gate,
65
+ trigger=DetectorTrigger.CONSTANT_GATE,
66
66
  deadtime=deadtime,
67
67
  livetime=exposure,
68
68
  frame_timeout=frame_timeout,
@@ -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,7 +16,14 @@ 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
@@ -174,8 +181,11 @@ async def infer_signal_type(
174
181
  else:
175
182
  dev_proxy = proxy
176
183
 
177
- if tr_name in dev_proxy.get_pipe_list():
178
- raise NotImplementedError("Pipes are not supported")
184
+ try:
185
+ if tr_name in dev_proxy.get_pipe_list():
186
+ raise NotImplementedError("Pipes are not supported")
187
+ except NonSupportedFeature: # type: ignore
188
+ pass
179
189
 
180
190
  if tr_name not in dev_proxy.get_attribute_list():
181
191
  if tr_name not in dev_proxy.get_command_list():
@@ -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
  """