ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.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 (151) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +97 -62
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +106 -125
  8. ophyd_async/core/_device.py +69 -63
  9. ophyd_async/core/_device_filler.py +65 -1
  10. ophyd_async/core/_flyer.py +14 -5
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +44 -35
  17. ophyd_async/core/_settings.py +36 -27
  18. ophyd_async/core/_signal.py +262 -170
  19. ophyd_async/core/_signal_backend.py +56 -13
  20. ophyd_async/core/_soft_signal_backend.py +16 -11
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +37 -8
  23. ophyd_async/core/_utils.py +96 -49
  24. ophyd_async/core/_yaml_settings.py +2 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/_andor.py +2 -2
  27. ophyd_async/epics/adandor/_andor_controller.py +4 -2
  28. ophyd_async/epics/adandor/_andor_io.py +2 -4
  29. ophyd_async/epics/adaravis/__init__.py +5 -0
  30. ophyd_async/epics/adaravis/_aravis.py +4 -8
  31. ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
  32. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  33. ophyd_async/epics/adcore/__init__.py +23 -8
  34. ophyd_async/epics/adcore/_core_detector.py +42 -2
  35. ophyd_async/epics/adcore/_core_io.py +124 -99
  36. ophyd_async/epics/adcore/_core_logic.py +106 -27
  37. ophyd_async/epics/adcore/_core_writer.py +12 -8
  38. ophyd_async/epics/adcore/_hdf_writer.py +21 -38
  39. ophyd_async/epics/adcore/_single_trigger.py +2 -2
  40. ophyd_async/epics/adcore/_utils.py +2 -2
  41. ophyd_async/epics/adkinetix/__init__.py +2 -1
  42. ophyd_async/epics/adkinetix/_kinetix.py +3 -3
  43. ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
  44. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  45. ophyd_async/epics/adpilatus/__init__.py +5 -0
  46. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  47. ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
  48. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  49. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  50. ophyd_async/epics/adsimdetector/_sim.py +4 -14
  51. ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
  52. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  53. ophyd_async/epics/advimba/__init__.py +10 -1
  54. ophyd_async/epics/advimba/_vimba.py +3 -2
  55. ophyd_async/epics/advimba/_vimba_controller.py +4 -2
  56. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  57. ophyd_async/epics/core/_aioca.py +35 -16
  58. ophyd_async/epics/core/_epics_connector.py +4 -0
  59. ophyd_async/epics/core/_epics_device.py +2 -0
  60. ophyd_async/epics/core/_p4p.py +10 -2
  61. ophyd_async/epics/core/_pvi_connector.py +65 -8
  62. ophyd_async/epics/core/_signal.py +51 -51
  63. ophyd_async/epics/core/_util.py +4 -4
  64. ophyd_async/epics/demo/__init__.py +16 -0
  65. ophyd_async/epics/demo/__main__.py +31 -0
  66. ophyd_async/epics/demo/_ioc.py +32 -0
  67. ophyd_async/epics/demo/_motor.py +82 -0
  68. ophyd_async/epics/demo/_point_detector.py +42 -0
  69. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  70. ophyd_async/epics/demo/_stage.py +15 -0
  71. ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
  72. ophyd_async/epics/demo/point_detector.db +59 -0
  73. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  74. ophyd_async/epics/eiger/_eiger.py +1 -3
  75. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  76. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  77. ophyd_async/epics/eiger/_odin_io.py +1 -2
  78. ophyd_async/epics/motor.py +65 -28
  79. ophyd_async/epics/signal.py +4 -1
  80. ophyd_async/epics/testing/_example_ioc.py +21 -9
  81. ophyd_async/epics/testing/_utils.py +3 -0
  82. ophyd_async/epics/testing/test_records.db +8 -0
  83. ophyd_async/epics/testing/test_records_pva.db +17 -16
  84. ophyd_async/fastcs/__init__.py +1 -0
  85. ophyd_async/fastcs/core.py +6 -0
  86. ophyd_async/fastcs/odin/__init__.py +1 -0
  87. ophyd_async/fastcs/panda/__init__.py +8 -6
  88. ophyd_async/fastcs/panda/_block.py +29 -9
  89. ophyd_async/fastcs/panda/_control.py +5 -0
  90. ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
  91. ophyd_async/fastcs/panda/_table.py +9 -6
  92. ophyd_async/fastcs/panda/_trigger.py +23 -9
  93. ophyd_async/fastcs/panda/_writer.py +27 -30
  94. ophyd_async/plan_stubs/__init__.py +2 -0
  95. ophyd_async/plan_stubs/_ensure_connected.py +1 -0
  96. ophyd_async/plan_stubs/_fly.py +2 -4
  97. ophyd_async/plan_stubs/_nd_attributes.py +2 -0
  98. ophyd_async/plan_stubs/_panda.py +1 -0
  99. ophyd_async/plan_stubs/_settings.py +43 -16
  100. ophyd_async/plan_stubs/_utils.py +3 -0
  101. ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
  102. ophyd_async/sim/__init__.py +24 -14
  103. ophyd_async/sim/__main__.py +43 -0
  104. ophyd_async/sim/_blob_detector.py +33 -0
  105. ophyd_async/sim/_blob_detector_controller.py +48 -0
  106. ophyd_async/sim/_blob_detector_writer.py +105 -0
  107. ophyd_async/sim/_mirror_horizontal.py +46 -0
  108. ophyd_async/sim/_mirror_vertical.py +74 -0
  109. ophyd_async/sim/_motor.py +233 -0
  110. ophyd_async/sim/_pattern_generator.py +124 -0
  111. ophyd_async/sim/_point_detector.py +86 -0
  112. ophyd_async/sim/_stage.py +19 -0
  113. ophyd_async/tango/__init__.py +1 -0
  114. ophyd_async/tango/core/__init__.py +6 -1
  115. ophyd_async/tango/core/_base_device.py +41 -33
  116. ophyd_async/tango/core/_converters.py +81 -0
  117. ophyd_async/tango/core/_signal.py +18 -32
  118. ophyd_async/tango/core/_tango_readable.py +2 -19
  119. ophyd_async/tango/core/_tango_transport.py +136 -60
  120. ophyd_async/tango/core/_utils.py +47 -0
  121. ophyd_async/tango/{sim → demo}/_counter.py +2 -0
  122. ophyd_async/tango/{sim → demo}/_detector.py +2 -0
  123. ophyd_async/tango/{sim → demo}/_mover.py +5 -4
  124. ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
  125. ophyd_async/tango/testing/__init__.py +6 -0
  126. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  127. ophyd_async/testing/__init__.py +29 -7
  128. ophyd_async/testing/_assert.py +137 -81
  129. ophyd_async/testing/_mock_signal_utils.py +56 -70
  130. ophyd_async/testing/_one_of_everything.py +41 -21
  131. ophyd_async/testing/_single_derived.py +87 -0
  132. ophyd_async/testing/_utils.py +3 -0
  133. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
  134. ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
  135. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
  136. ophyd_async/epics/sim/__init__.py +0 -54
  137. ophyd_async/epics/sim/_ioc.py +0 -29
  138. ophyd_async/epics/sim/_mover.py +0 -101
  139. ophyd_async/epics/sim/_sensor.py +0 -37
  140. ophyd_async/epics/sim/sensor.db +0 -19
  141. ophyd_async/sim/_pattern_detector/__init__.py +0 -13
  142. ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
  143. ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
  144. ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
  145. ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
  146. ophyd_async/sim/_sim_motor.py +0 -107
  147. ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
  148. /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
  149. /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
  150. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
  151. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,15 @@
1
1
  import asyncio
2
2
  import functools
3
+ import logging
3
4
  import time
4
5
  from abc import abstractmethod
5
6
  from collections.abc import Callable, Coroutine
6
7
  from enum import Enum
7
- from typing import Any, TypeVar, cast
8
+ from typing import Any, ParamSpec, TypeVar, cast
8
9
 
9
10
  import numpy as np
10
- from bluesky.protocols import Descriptor, Reading
11
+ from bluesky.protocols import Reading
12
+ from event_model import DataKey
11
13
 
12
14
  from ophyd_async.core import (
13
15
  AsyncStatus,
@@ -15,6 +17,7 @@ from ophyd_async.core import (
15
17
  NotConnected,
16
18
  SignalBackend,
17
19
  SignalDatatypeT,
20
+ StrictEnum,
18
21
  get_dtype,
19
22
  get_unique,
20
23
  wait_for_connection,
@@ -37,26 +40,41 @@ from tango.asyncio_executor import (
37
40
  )
38
41
  from tango.utils import is_array, is_binary, is_bool, is_float, is_int, is_str
39
42
 
43
+ from ._converters import (
44
+ TangoConverter,
45
+ TangoDevStateArrayConverter,
46
+ TangoDevStateConverter,
47
+ TangoEnumArrayConverter,
48
+ TangoEnumConverter,
49
+ )
50
+ from ._utils import DevStateEnum, get_device_trl_and_attr
51
+
52
+ logger = logging.getLogger("ophyd_async")
53
+
40
54
  # time constant to wait for timeout
41
55
  A_BIT = 1e-5
42
56
 
57
+ P = ParamSpec("P")
43
58
  R = TypeVar("R")
44
59
 
45
60
 
46
61
  def ensure_proper_executor(
47
- func: Callable[..., Coroutine[Any, Any, R]],
48
- ) -> Callable[..., Coroutine[Any, Any, R]]:
62
+ func: Callable[P, Coroutine[Any, Any, R]],
63
+ ) -> Callable[P, Coroutine[Any, Any, R]]:
64
+ """Ensure decorated method has a proper asyncio executor."""
65
+
49
66
  @functools.wraps(func)
50
- async def wrapper(self: Any, *args: Any, **kwargs: Any) -> R:
67
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
51
68
  current_executor: AsyncioExecutor = get_global_executor() # type: ignore
52
69
  if not current_executor.in_executor_context(): # type: ignore
53
70
  set_global_executor(AsyncioExecutor())
54
- return await func(self, *args, **kwargs)
71
+ return await func(*args, **kwargs)
55
72
 
56
- return cast(Callable[..., Coroutine[Any, Any, R]], wrapper)
73
+ return wrapper
57
74
 
58
75
 
59
76
  def get_python_type(tango_type: CmdArgType) -> tuple[bool, object, str]:
77
+ """For converting between recieved tango types and python primatives."""
60
78
  array = is_array(tango_type)
61
79
  if is_int(tango_type, True):
62
80
  return array, int, "integer"
@@ -83,48 +101,51 @@ class TangoProxy:
83
101
  support_events: bool = True
84
102
  _proxy: DeviceProxy
85
103
  _name: str
104
+ _converter: TangoConverter = TangoConverter()
86
105
 
87
106
  def __init__(self, device_proxy: DeviceProxy, name: str):
88
107
  self._proxy = device_proxy
89
108
  self._name = name
90
109
 
91
110
  async def connect(self) -> None:
92
- """perform actions after proxy is connected, e.g. checks if signal
93
- can be subscribed"""
111
+ """Perform actions after proxy is connected.
112
+
113
+ e.g. check if signal can be subscribed.
114
+ """
94
115
 
95
116
  @abstractmethod
96
117
  async def get(self) -> object:
97
- """Get value from TRL"""
118
+ """Get value from TRL."""
98
119
 
99
120
  @abstractmethod
100
121
  async def get_w_value(self) -> object:
101
- """Get last written value from TRL"""
122
+ """Get last written value from TRL."""
102
123
 
103
124
  @abstractmethod
104
125
  async def put(
105
126
  self, value: object | None, wait: bool = True, timeout: float | None = None
106
127
  ) -> AsyncStatus | None:
107
- """Put value to TRL"""
128
+ """Put value to TRL."""
108
129
 
109
130
  @abstractmethod
110
131
  async def get_config(self) -> AttributeInfoEx | CommandInfo:
111
- """Get TRL config async"""
132
+ """Get TRL config async."""
112
133
 
113
134
  @abstractmethod
114
135
  async def get_reading(self) -> Reading:
115
- """Get reading from TRL"""
136
+ """Get reading from TRL."""
116
137
 
117
138
  @abstractmethod
118
139
  def has_subscription(self) -> bool:
119
- """indicates, that this trl already subscribed"""
140
+ """Indicate that this trl already subscribed."""
120
141
 
121
142
  @abstractmethod
122
143
  def subscribe_callback(self, callback: Callback | None):
123
- """subscribe tango CHANGE event to callback"""
144
+ """Subscribe tango CHANGE event to callback."""
124
145
 
125
146
  @abstractmethod
126
147
  def unsubscribe_callback(self):
127
- """delete CHANGE event subscription"""
148
+ """Delete CHANGE event subscription."""
128
149
 
129
150
  @abstractmethod
130
151
  def set_polling(
@@ -134,10 +155,15 @@ class TangoProxy:
134
155
  abs_change=None,
135
156
  rel_change=None,
136
157
  ):
137
- """Set polling parameters"""
158
+ """Set polling parameters."""
159
+
160
+ def set_converter(self, converter: "TangoConverter"):
161
+ self._converter = converter
138
162
 
139
163
 
140
164
  class AttributeProxy(TangoProxy):
165
+ """Used by the tango transport."""
166
+
141
167
  _callback: Callback | None = None
142
168
  _eid: int | None = None
143
169
  _poll_task: asyncio.Task | None = None
@@ -163,20 +189,21 @@ class AttributeProxy(TangoProxy):
163
189
  pass
164
190
 
165
191
  @ensure_proper_executor
166
- async def get(self) -> Coroutine[Any, Any, object]:
192
+ async def get(self) -> object: # type: ignore
167
193
  attr = await self._proxy.read_attribute(self._name)
168
- return attr.value
194
+ return self._converter.value(attr.value)
169
195
 
170
196
  @ensure_proper_executor
171
- async def get_w_value(self) -> object:
197
+ async def get_w_value(self) -> object: # type: ignore
172
198
  attr = await self._proxy.read_attribute(self._name)
173
- return attr.w_value
199
+ return self._converter.value(attr.w_value)
174
200
 
175
201
  @ensure_proper_executor
176
- async def put(
202
+ async def put( # type: ignore
177
203
  self, value: object | None, wait: bool = True, timeout: float | None = None
178
204
  ) -> AsyncStatus | None:
179
205
  # TODO: remove the timeout from this as it is handled at the signal level
206
+ value = self._converter.write_value(value)
180
207
  if wait:
181
208
  try:
182
209
 
@@ -220,14 +247,16 @@ class AttributeProxy(TangoProxy):
220
247
  return AsyncStatus(wait_for_reply(rid, timeout))
221
248
 
222
249
  @ensure_proper_executor
223
- async def get_config(self) -> AttributeInfoEx:
250
+ async def get_config(self) -> AttributeInfoEx: # type: ignore
224
251
  return await self._proxy.get_attribute_config(self._name)
225
252
 
226
253
  @ensure_proper_executor
227
- async def get_reading(self) -> Reading:
254
+ async def get_reading(self) -> Reading: # type: ignore
228
255
  attr = await self._proxy.read_attribute(self._name)
229
256
  reading = Reading(
230
- value=attr.value, timestamp=attr.time.totime(), alarm_severity=attr.quality
257
+ value=self._converter.value(attr.value),
258
+ timestamp=attr.time.totime(),
259
+ alarm_severity=attr.quality,
231
260
  )
232
261
  self._last_reading = reading
233
262
  return reading
@@ -290,7 +319,7 @@ class AttributeProxy(TangoProxy):
290
319
  def _event_processor(self, event):
291
320
  if not event.err:
292
321
  reading = Reading(
293
- value=event.attr_value.value,
322
+ value=self._converter.value(event.attr_value.value),
294
323
  timestamp=event.get_date().totime(),
295
324
  alarm_severity=event.attr_value.quality,
296
325
  )
@@ -298,10 +327,11 @@ class AttributeProxy(TangoProxy):
298
327
  self._callback(reading)
299
328
 
300
329
  async def poll(self):
301
- """
302
- Poll the attribute and call the callback if the value has changed by more
303
- than the absolute or relative change. This function is used when an attribute
304
- that does not support events is cached or a callback is passed to it.
330
+ """Poll the attribute and call the callback if the value has changed.
331
+
332
+ Only callback if value has changed by more than the absolute or relative
333
+ change. This function is used when an attribute that does not support
334
+ events is cached or a callback is passed to it.
305
335
  """
306
336
  try:
307
337
  last_reading = await self.get_reading()
@@ -376,9 +406,7 @@ class AttributeProxy(TangoProxy):
376
406
  abs_change: float | None = None,
377
407
  rel_change: float | None = 0.1,
378
408
  ):
379
- """
380
- Set the polling parameters.
381
- """
409
+ """Set the polling parameters."""
382
410
  self._allow_polling = allow_polling
383
411
  self._polling_period = polling_period
384
412
  self._abs_change = abs_change
@@ -386,6 +414,8 @@ class AttributeProxy(TangoProxy):
386
414
 
387
415
 
388
416
  class CommandProxy(TangoProxy):
417
+ """Tango proxy for commands."""
418
+
389
419
  _last_reading: Reading = Reading(value=None, timestamp=0, alarm_severity=0)
390
420
 
391
421
  def subscribe_callback(self, callback: Callback | None) -> None:
@@ -404,9 +434,10 @@ class CommandProxy(TangoProxy):
404
434
  pass
405
435
 
406
436
  @ensure_proper_executor
407
- async def put(
437
+ async def put( # type: ignore
408
438
  self, value: object | None, wait: bool = True, timeout: float | None = None
409
439
  ) -> AsyncStatus | None:
440
+ value = self._converter.write_value(value)
410
441
  if wait:
411
442
  try:
412
443
 
@@ -416,7 +447,9 @@ class CommandProxy(TangoProxy):
416
447
  task = asyncio.create_task(_put())
417
448
  val = await asyncio.wait_for(task, timeout)
418
449
  self._last_reading = Reading(
419
- value=val, timestamp=time.time(), alarm_severity=0
450
+ value=self._converter.value(val),
451
+ timestamp=time.time(),
452
+ alarm_severity=0,
420
453
  )
421
454
  except asyncio.TimeoutError as te:
422
455
  raise TimeoutError(f"{self._name} command failed: Timeout") from te
@@ -432,7 +465,9 @@ class CommandProxy(TangoProxy):
432
465
  start_time = time.time()
433
466
  while True:
434
467
  try:
435
- reply_value = self._proxy.command_inout_reply(rd)
468
+ reply_value = self._converter.value(
469
+ self._proxy.command_inout_reply(rd)
470
+ )
436
471
  self._last_reading = Reading(
437
472
  value=reply_value, timestamp=time.time(), alarm_severity=0
438
473
  )
@@ -452,7 +487,7 @@ class CommandProxy(TangoProxy):
452
487
  return AsyncStatus(wait_for_reply(rid, timeout))
453
488
 
454
489
  @ensure_proper_executor
455
- async def get_config(self) -> CommandInfo:
490
+ async def get_config(self) -> CommandInfo: # type: ignore
456
491
  return await self._proxy.get_command_config(self._name)
457
492
 
458
493
  async def get_reading(self) -> Reading:
@@ -474,10 +509,11 @@ class CommandProxy(TangoProxy):
474
509
 
475
510
 
476
511
  def get_dtype_extended(datatype) -> object | None:
512
+ """For converting tango types to numpy datatype formats."""
477
513
  # DevState tango type does not have numpy equivalents
478
514
  dtype = get_dtype(datatype)
479
515
  if dtype == np.object_:
480
- if datatype.__args__[1].__args__[0] == DevState:
516
+ if datatype.__args__[1].__args__[0] in [DevStateEnum, DevState]:
481
517
  dtype = CmdArgType.DevState
482
518
  return dtype
483
519
 
@@ -486,7 +522,8 @@ def get_trl_descriptor(
486
522
  datatype: type | None,
487
523
  tango_resource: str,
488
524
  tr_configs: dict[str, AttributeInfoEx | CommandInfo],
489
- ) -> Descriptor:
525
+ ) -> DataKey:
526
+ """Create a descriptor from a tango resource locator."""
490
527
  tr_dtype = {}
491
528
  for tr_name, config in tr_configs.items():
492
529
  if isinstance(config, AttributeInfoEx):
@@ -544,11 +581,9 @@ def get_trl_descriptor(
544
581
  raise TypeError(f"{tango_resource} has type [{tr_dtype}] not [{dtype}]")
545
582
 
546
583
  if tr_format == AttrDataFormat.SPECTRUM:
547
- return Descriptor(source=tango_resource, dtype="array", shape=[max_x])
584
+ return DataKey(source=tango_resource, dtype="array", shape=[max_x])
548
585
  elif tr_format == AttrDataFormat.IMAGE:
549
- return Descriptor(
550
- source=tango_resource, dtype="array", shape=[max_y, max_x]
551
- )
586
+ return DataKey(source=tango_resource, dtype="array", shape=[max_y, max_x])
552
587
 
553
588
  else:
554
589
  if tr_dtype in (Enum, CmdArgType.DevState):
@@ -568,14 +603,14 @@ def get_trl_descriptor(
568
603
  # f"{tango_resource} has choices {trl_choices} "
569
604
  # f"not {choices}"
570
605
  # )
571
- return Descriptor(source=tango_resource, dtype="string", shape=[])
606
+ return DataKey(source=tango_resource, dtype="string", shape=[])
572
607
  else:
573
608
  if datatype and not issubclass(tr_dtype, datatype):
574
609
  raise TypeError(
575
610
  f"{tango_resource} has type {tr_dtype.__name__} "
576
611
  f"not {datatype.__name__}"
577
612
  )
578
- return Descriptor(source=tango_resource, dtype=tr_dtype_desc, shape=[])
613
+ return DataKey(source=tango_resource, dtype=tr_dtype_desc, shape=[])
579
614
 
580
615
  raise RuntimeError(f"Error getting descriptor for {tango_resource}")
581
616
 
@@ -583,9 +618,10 @@ def get_trl_descriptor(
583
618
  async def get_tango_trl(
584
619
  full_trl: str, device_proxy: DeviceProxy | TangoProxy | None, timeout: float
585
620
  ) -> TangoProxy:
621
+ """Get the tango resource locator."""
586
622
  if isinstance(device_proxy, TangoProxy):
587
623
  return device_proxy
588
- device_trl, trl_name = full_trl.rsplit("/", 1)
624
+ device_trl, trl_name = get_device_trl_and_attr(full_trl)
589
625
  trl_name = trl_name.lower()
590
626
  if device_proxy is None:
591
627
  device_proxy = await AsyncDeviceProxy(device_trl, timeout=timeout)
@@ -607,17 +643,51 @@ async def get_tango_trl(
607
643
  if trl_name in all_cmds:
608
644
  return CommandProxy(device_proxy, trl_name)
609
645
 
610
- # If version is below tango 9, then pipes are not supported
611
- if device_proxy.info().server_version >= 9:
612
- # all pipes can be always accessible with low register
613
- all_pipes = [pipe_name.lower() for pipe_name in device_proxy.get_pipe_list()]
614
- if trl_name in all_pipes:
615
- raise NotImplementedError("Pipes are not supported")
616
-
617
646
  raise RuntimeError(f"{trl_name} cannot be found in {device_proxy.name()}")
618
647
 
619
648
 
649
+ def make_converter(info: AttributeInfoEx | CommandInfo, datatype) -> TangoConverter:
650
+ if isinstance(info, AttributeInfoEx):
651
+ match info.data_type:
652
+ case CmdArgType.DevEnum:
653
+ if datatype and issubclass(datatype, StrictEnum):
654
+ labels = [e.value for e in datatype]
655
+ else: # get from enum_labels metadata
656
+ labels = list(info.enum_labels)
657
+ if info.data_format == AttrDataFormat.SCALAR:
658
+ return TangoEnumConverter(labels)
659
+ elif info.data_format in [
660
+ AttrDataFormat.SPECTRUM,
661
+ AttrDataFormat.IMAGE,
662
+ ]:
663
+ return TangoEnumArrayConverter(labels)
664
+ case CmdArgType.DevState:
665
+ if info.data_format == AttrDataFormat.SCALAR:
666
+ return TangoDevStateConverter()
667
+ elif info.data_format in [
668
+ AttrDataFormat.SPECTRUM,
669
+ AttrDataFormat.IMAGE,
670
+ ]:
671
+ return TangoDevStateArrayConverter()
672
+ else: # command info
673
+ match info.in_type:
674
+ case CmdArgType.DevState:
675
+ return TangoDevStateConverter()
676
+ case CmdArgType.DevEnum:
677
+ if datatype and issubclass(datatype, StrictEnum):
678
+ labels = [e.value for e in datatype]
679
+ return TangoEnumConverter(labels)
680
+ else:
681
+ logger.warning(
682
+ "No override enum class provided for Tango enum command"
683
+ )
684
+ # default case return trivial converter
685
+ return TangoConverter()
686
+
687
+
620
688
  class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
689
+ """Tango backend to connect signals over tango."""
690
+
621
691
  def __init__(
622
692
  self,
623
693
  datatype: type[SignalDatatypeT] | None,
@@ -633,7 +703,7 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
633
703
  write_trl: self.device_proxy,
634
704
  }
635
705
  self.trl_configs: dict[str, AttributeInfoEx] = {}
636
- self.descriptor: Descriptor = {} # type: ignore
706
+ self.descriptor: DataKey = {} # type: ignore
637
707
  self._polling: tuple[bool, float, float | None, float | None] = (
638
708
  False,
639
709
  0.1,
@@ -642,11 +712,12 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
642
712
  )
643
713
  self.support_events: bool = True
644
714
  self.status: AsyncStatus | None = None
715
+ self.converter = TangoConverter() # gets replaced at connect
645
716
  super().__init__(datatype)
646
717
 
647
718
  @classmethod
648
719
  def datatype_allowed(cls, dtype: Any) -> bool:
649
- return dtype in (int, float, str, bool, np.ndarray, Enum, DevState)
720
+ return dtype in (int, float, str, bool, np.ndarray, StrictEnum)
650
721
 
651
722
  def set_trl(self, read_trl: str = "", write_trl: str = ""):
652
723
  self.read_trl = read_trl
@@ -687,6 +758,8 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
687
758
  # The same, so only need to connect one
688
759
  await self._connect_and_store_config(self.read_trl, timeout)
689
760
  self.proxies[self.read_trl].set_polling(*self._polling) # type: ignore
761
+ self.converter = make_converter(self.trl_configs[self.read_trl], self.datatype)
762
+ self.proxies[self.read_trl].set_converter(self.converter) # type: ignore
690
763
  self.descriptor = get_trl_descriptor(
691
764
  self.datatype, self.read_trl, self.trl_configs
692
765
  )
@@ -698,13 +771,14 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
698
771
  put_status = await self.proxies[self.write_trl].put(value, wait, timeout) # type: ignore
699
772
  self.status = put_status
700
773
 
701
- async def get_datakey(self, source: str) -> Descriptor:
774
+ async def get_datakey(self, source: str) -> DataKey:
702
775
  return self.descriptor
703
776
 
704
777
  async def get_reading(self) -> Reading[SignalDatatypeT]:
705
778
  if self.proxies[self.read_trl] is None:
706
779
  raise NotConnected(f"Not connected to {self.read_trl}")
707
- return await self.proxies[self.read_trl].get_reading() # type: ignore
780
+ reading = await self.proxies[self.read_trl].get_reading() # type: ignore
781
+ return reading
708
782
 
709
783
  async def get_value(self) -> SignalDatatypeT:
710
784
  if self.proxies[self.read_trl] is None:
@@ -712,7 +786,8 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
712
786
  proxy = self.proxies[self.read_trl]
713
787
  if proxy is None:
714
788
  raise NotConnected(f"Not connected to {self.read_trl}")
715
- return cast(SignalDatatypeT, await proxy.get())
789
+ value = await proxy.get()
790
+ return cast(SignalDatatypeT, value)
716
791
 
717
792
  async def get_setpoint(self) -> SignalDatatypeT:
718
793
  if self.proxies[self.write_trl] is None:
@@ -720,7 +795,8 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
720
795
  proxy = self.proxies[self.write_trl]
721
796
  if proxy is None:
722
797
  raise NotConnected(f"Not connected to {self.write_trl}")
723
- return cast(SignalDatatypeT, await proxy.get_w_value())
798
+ w_value = await proxy.get_w_value()
799
+ return cast(SignalDatatypeT, w_value)
724
800
 
725
801
  def set_callback(self, callback: Callback | None) -> None:
726
802
  if self.proxies[self.read_trl] is None:
@@ -0,0 +1,47 @@
1
+ import re
2
+
3
+ from ophyd_async.core import StrictEnum
4
+
5
+
6
+ class DevStateEnum(StrictEnum):
7
+ ON = "ON"
8
+ OFF = "OFF"
9
+ CLOSE = "CLOSE"
10
+ OPEN = "OPEN"
11
+ INSERT = "INSERT"
12
+ EXTRACT = "EXTRACT"
13
+ MOVING = "MOVING"
14
+ STANDBY = "STANDBY"
15
+ FAULT = "FAULT"
16
+ INIT = "INIT"
17
+ RUNNING = "RUNNING"
18
+ ALARM = "ALARM"
19
+ DISABLE = "DISABLE"
20
+ UNKNOWN = "UNKNOWN"
21
+
22
+
23
+ def get_full_attr_trl(device_trl: str, attr_name: str):
24
+ device_parts = device_trl.split("#", 1)
25
+ # my/device/name#dbase=no splits into my/device/name and dbase=no
26
+ full_trl = device_parts[0] + "/" + attr_name
27
+ if len(device_parts) > 1:
28
+ full_trl += "#" + device_parts[1]
29
+ return full_trl
30
+
31
+
32
+ def get_device_trl_and_attr(name: str):
33
+ # trl can have form:
34
+ # <protocol://><server:host/>domain/family/member/attr_name<#dbase=no>
35
+ # e.g. tango://127.0.0.1:8888/test/nodb/test#dbase=no
36
+ re_str = (
37
+ r"([\.a-zA-Z0-9_-]*://)?([\.a-zA-Z0-9_-]+:[0-9]+/)?"
38
+ r"([^#/]+/[^#/]+/[^#/]+/)([^#/]+)(#dbase=[a-z]+)?"
39
+ )
40
+ search = re.search(re_str, name)
41
+ if not search:
42
+ raise ValueError(f"Could not parse device and attribute from trl {name}")
43
+ groups = [part if part else "" for part in search.groups()]
44
+ attr = groups.pop(3) # extract attr name from groups
45
+ groups[2] = groups[2].removesuffix("/") # remove trailing slash from device name
46
+ device = "".join(groups)
47
+ return device, attr
@@ -6,6 +6,8 @@ from ophyd_async.tango.core import TangoPolling, TangoReadable
6
6
 
7
7
 
8
8
  class TangoCounter(TangoReadable):
9
+ """Tango counting device."""
10
+
9
11
  # Enter the name and type of the signals you want to use
10
12
  # If the server doesn't support events, the TangoPolling annotation gives
11
13
  # the parameters for ophyd to poll instead
@@ -11,6 +11,8 @@ from ._mover import TangoMover
11
11
 
12
12
 
13
13
  class TangoDetector(StandardReadable):
14
+ """For use with tango detector devices."""
15
+
14
16
  def __init__(self, mover_trl: str, counter_trls: list[str], name=""):
15
17
  # A detector device may be composed of tango sub-devices
16
18
  self.mover = TangoMover(mover_trl)
@@ -17,17 +17,18 @@ from ophyd_async.core import (
17
17
  wait_for_value,
18
18
  )
19
19
  from ophyd_async.core import StandardReadableFormat as Format
20
- from ophyd_async.tango.core import TangoPolling, TangoReadable
21
- from tango import DevState
20
+ from ophyd_async.tango.core import DevStateEnum, TangoPolling, TangoReadable
22
21
 
23
22
 
24
23
  class TangoMover(TangoReadable, Movable, Stoppable):
24
+ """Tango moving device."""
25
+
25
26
  # Enter the name and type of the signals you want to use
26
27
  # If the server doesn't support events, the TangoPolling annotation gives
27
28
  # the parameters for ophyd to poll instead
28
29
  position: A[SignalRW[float], TangoPolling(0.1, 0.1, 0.1)]
29
30
  velocity: A[SignalRW[float], TangoPolling(0.1, 0.1, 0.1)]
30
- state: A[SignalR[DevState], TangoPolling(0.1)]
31
+ state: A[SignalR[DevStateEnum], TangoPolling(0.1)]
31
32
  # If a tango name clashes with a bluesky verb, add a trailing underscore
32
33
  stop_: SignalX
33
34
 
@@ -56,7 +57,7 @@ class TangoMover(TangoReadable, Movable, Stoppable):
56
57
  await self.position.set(value, wait=False, timeout=timeout)
57
58
 
58
59
  move_status = AsyncStatus(
59
- wait_for_value(self.state, DevState.ON, timeout=timeout)
60
+ wait_for_value(self.state, DevStateEnum.ON, timeout=timeout)
60
61
  )
61
62
 
62
63
  try:
@@ -8,6 +8,8 @@ from tango.server import Device, attribute, command
8
8
 
9
9
 
10
10
  class DemoMover(Device):
11
+ """Demo tango moving device."""
12
+
11
13
  green_mode = GreenMode.Asyncio
12
14
  _position = 0.0
13
15
  _setpoint = 0.0
@@ -65,6 +67,8 @@ class DemoMover(Device):
65
67
 
66
68
 
67
69
  class DemoCounter(Device):
70
+ """Demo tango counting device."""
71
+
68
72
  green_mode = GreenMode.Asyncio
69
73
  _counts = 0
70
74
  _sample_time = 1.0
@@ -0,0 +1,6 @@
1
+ from ._one_of_everything import (
2
+ ExampleStrEnum,
3
+ OneOfEverythingTangoDevice,
4
+ )
5
+
6
+ __all__ = ["ExampleStrEnum", "OneOfEverythingTangoDevice"]