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
@@ -0,0 +1,764 @@
1
+ import asyncio
2
+ import functools
3
+ import time
4
+ from abc import abstractmethod
5
+ from asyncio import CancelledError
6
+ from collections.abc import Callable, Coroutine
7
+ from enum import Enum
8
+ from typing import Any, TypeVar, cast
9
+
10
+ import numpy as np
11
+ from bluesky.protocols import Descriptor, Reading
12
+
13
+ from ophyd_async.core import (
14
+ DEFAULT_TIMEOUT,
15
+ AsyncStatus,
16
+ NotConnected,
17
+ ReadingValueCallback,
18
+ SignalBackend,
19
+ T,
20
+ get_dtype,
21
+ get_unique,
22
+ wait_for_connection,
23
+ )
24
+ from tango import (
25
+ AttrDataFormat,
26
+ AttributeInfoEx,
27
+ CmdArgType,
28
+ CommandInfo,
29
+ DevFailed, # type: ignore
30
+ DeviceProxy,
31
+ DevState,
32
+ EventType,
33
+ )
34
+ from tango.asyncio import DeviceProxy as AsyncDeviceProxy
35
+ from tango.asyncio_executor import (
36
+ AsyncioExecutor,
37
+ get_global_executor,
38
+ set_global_executor,
39
+ )
40
+ from tango.utils import is_array, is_binary, is_bool, is_float, is_int, is_str
41
+
42
+ # time constant to wait for timeout
43
+ A_BIT = 1e-5
44
+
45
+ R = TypeVar("R")
46
+
47
+
48
+ def ensure_proper_executor(
49
+ func: Callable[..., Coroutine[Any, Any, R]],
50
+ ) -> Callable[..., Coroutine[Any, Any, R]]:
51
+ @functools.wraps(func)
52
+ async def wrapper(self: Any, *args: Any, **kwargs: Any) -> R:
53
+ current_executor: AsyncioExecutor = get_global_executor() # type: ignore
54
+ if not current_executor.in_executor_context(): # type: ignore
55
+ set_global_executor(AsyncioExecutor())
56
+ return await func(self, *args, **kwargs)
57
+
58
+ return cast(Callable[..., Coroutine[Any, Any, R]], wrapper)
59
+
60
+
61
+ def get_python_type(tango_type: CmdArgType) -> tuple[bool, object, str]:
62
+ array = is_array(tango_type)
63
+ if is_int(tango_type, True):
64
+ return array, int, "integer"
65
+ if is_float(tango_type, True):
66
+ return array, float, "number"
67
+ if is_bool(tango_type, True):
68
+ return array, bool, "integer"
69
+ if is_str(tango_type, True):
70
+ return array, str, "string"
71
+ if is_binary(tango_type, True):
72
+ return array, list[str], "string"
73
+ if tango_type == CmdArgType.DevEnum:
74
+ return array, Enum, "string"
75
+ if tango_type == CmdArgType.DevState:
76
+ return array, CmdArgType.DevState, "string"
77
+ if tango_type == CmdArgType.DevUChar:
78
+ return array, int, "integer"
79
+ if tango_type == CmdArgType.DevVoid:
80
+ return array, None, "string"
81
+ raise TypeError("Unknown TangoType")
82
+
83
+
84
+ class TangoProxy:
85
+ support_events: bool = True
86
+ _proxy: DeviceProxy
87
+ _name: str
88
+
89
+ def __init__(self, device_proxy: DeviceProxy, name: str):
90
+ self._proxy = device_proxy
91
+ self._name = name
92
+
93
+ async def connect(self) -> None:
94
+ """perform actions after proxy is connected, e.g. checks if signal
95
+ can be subscribed"""
96
+
97
+ @abstractmethod
98
+ async def get(self) -> object:
99
+ """Get value from TRL"""
100
+
101
+ @abstractmethod
102
+ async def get_w_value(self) -> object:
103
+ """Get last written value from TRL"""
104
+
105
+ @abstractmethod
106
+ async def put(
107
+ self, value: object | None, wait: bool = True, timeout: float | None = None
108
+ ) -> AsyncStatus | None:
109
+ """Put value to TRL"""
110
+
111
+ @abstractmethod
112
+ async def get_config(self) -> AttributeInfoEx | CommandInfo:
113
+ """Get TRL config async"""
114
+
115
+ @abstractmethod
116
+ async def get_reading(self) -> Reading:
117
+ """Get reading from TRL"""
118
+
119
+ @abstractmethod
120
+ def has_subscription(self) -> bool:
121
+ """indicates, that this trl already subscribed"""
122
+
123
+ @abstractmethod
124
+ def subscribe_callback(self, callback: ReadingValueCallback | None):
125
+ """subscribe tango CHANGE event to callback"""
126
+
127
+ @abstractmethod
128
+ def unsubscribe_callback(self):
129
+ """delete CHANGE event subscription"""
130
+
131
+ @abstractmethod
132
+ def set_polling(
133
+ self,
134
+ allow_polling: bool = True,
135
+ polling_period: float = 0.1,
136
+ abs_change=None,
137
+ rel_change=None,
138
+ ):
139
+ """Set polling parameters"""
140
+
141
+
142
+ class AttributeProxy(TangoProxy):
143
+ _callback: ReadingValueCallback | None = None
144
+ _eid: int | None = None
145
+ _poll_task: asyncio.Task | None = None
146
+ _abs_change: float | None = None
147
+ _rel_change: float | None = 0.1
148
+ _polling_period: float = 0.1
149
+ _allow_polling: bool = False
150
+ exception: BaseException | None = None
151
+ _last_reading: Reading = Reading(value=None, timestamp=0, alarm_severity=0)
152
+
153
+ async def connect(self) -> None:
154
+ try:
155
+ # I have to typehint proxy as tango.DeviceProxy because
156
+ # tango.asyncio.DeviceProxy cannot be used as a typehint.
157
+ # This means pyright will not be able to see that
158
+ # subscribe_event is awaitable.
159
+ eid = await self._proxy.subscribe_event( # type: ignore
160
+ self._name, EventType.CHANGE_EVENT, self._event_processor
161
+ )
162
+ await self._proxy.unsubscribe_event(eid)
163
+ self.support_events = True
164
+ except Exception:
165
+ pass
166
+
167
+ @ensure_proper_executor
168
+ async def get(self) -> Coroutine[Any, Any, object]:
169
+ attr = await self._proxy.read_attribute(self._name)
170
+ return attr.value
171
+
172
+ @ensure_proper_executor
173
+ async def get_w_value(self) -> object:
174
+ attr = await self._proxy.read_attribute(self._name)
175
+ return attr.w_value
176
+
177
+ @ensure_proper_executor
178
+ async def put(
179
+ self, value: object | None, wait: bool = True, timeout: float | None = None
180
+ ) -> AsyncStatus | None:
181
+ if wait:
182
+ try:
183
+
184
+ async def _write():
185
+ return await self._proxy.write_attribute(self._name, value)
186
+
187
+ task = asyncio.create_task(_write())
188
+ await asyncio.wait_for(task, timeout)
189
+ except asyncio.TimeoutError as te:
190
+ raise TimeoutError(f"{self._name} attr put failed: Timeout") from te
191
+ except DevFailed as de:
192
+ raise RuntimeError(
193
+ f"{self._name} device" f" failure: {de.args[0].desc}"
194
+ ) from de
195
+
196
+ else:
197
+ rid = await self._proxy.write_attribute_asynch(self._name, value)
198
+
199
+ async def wait_for_reply(rd: int, to: float | None):
200
+ start_time = time.time()
201
+ while True:
202
+ try:
203
+ # I have to typehint proxy as tango.DeviceProxy because
204
+ # tango.asyncio.DeviceProxy cannot be used as a typehint.
205
+ # This means pyright will not be able to see that
206
+ # write_attribute_reply is awaitable.
207
+ await self._proxy.write_attribute_reply(rd) # type: ignore
208
+ break
209
+ except DevFailed as exc:
210
+ if exc.args[0].reason == "API_AsynReplyNotArrived":
211
+ await asyncio.sleep(A_BIT)
212
+ if to and (time.time() - start_time > to):
213
+ raise TimeoutError(
214
+ f"{self._name} attr put failed:" f" Timeout"
215
+ ) from exc
216
+ else:
217
+ raise RuntimeError(
218
+ f"{self._name} device failure:" f" {exc.args[0].desc}"
219
+ ) from exc
220
+
221
+ return AsyncStatus(wait_for_reply(rid, timeout))
222
+
223
+ @ensure_proper_executor
224
+ async def get_config(self) -> AttributeInfoEx:
225
+ return await self._proxy.get_attribute_config(self._name)
226
+
227
+ @ensure_proper_executor
228
+ async def get_reading(self) -> Reading:
229
+ attr = await self._proxy.read_attribute(self._name)
230
+ reading = Reading(
231
+ value=attr.value, timestamp=attr.time.totime(), alarm_severity=attr.quality
232
+ )
233
+ self._last_reading = reading
234
+ return reading
235
+
236
+ def has_subscription(self) -> bool:
237
+ return bool(self._callback)
238
+
239
+ def subscribe_callback(self, callback: ReadingValueCallback | None):
240
+ # If the attribute supports events, then we can subscribe to them
241
+ # If the callback is not a callable, then we raise an error
242
+ if callback is not None and not callable(callback):
243
+ raise RuntimeError("Callback must be a callable")
244
+
245
+ self._callback = callback
246
+ if self.support_events:
247
+ """add user callback to CHANGE event subscription"""
248
+ if not self._eid:
249
+ self._eid = self._proxy.subscribe_event(
250
+ self._name,
251
+ EventType.CHANGE_EVENT,
252
+ self._event_processor,
253
+ green_mode=False,
254
+ )
255
+ elif self._allow_polling:
256
+ """start polling if no events supported"""
257
+ if self._callback is not None:
258
+
259
+ async def _poll():
260
+ while True:
261
+ try:
262
+ await self.poll()
263
+ except RuntimeError as exc:
264
+ self.exception = exc
265
+ await asyncio.sleep(1)
266
+
267
+ self._poll_task = asyncio.create_task(_poll())
268
+ else:
269
+ self.unsubscribe_callback()
270
+ raise RuntimeError(
271
+ f"Cannot set event for {self._name}. "
272
+ "Cannot set a callback on an attribute that does not support events and"
273
+ " for which polling is disabled."
274
+ )
275
+
276
+ def unsubscribe_callback(self):
277
+ if self._eid:
278
+ self._proxy.unsubscribe_event(self._eid, green_mode=False)
279
+ self._eid = None
280
+ if self._poll_task:
281
+ self._poll_task.cancel()
282
+ self._poll_task = None
283
+ if self._callback is not None:
284
+ # Call the callback with the last reading
285
+ try:
286
+ self._callback(self._last_reading, self._last_reading["value"])
287
+ except TypeError:
288
+ pass
289
+ self._callback = None
290
+
291
+ def _event_processor(self, event):
292
+ if not event.err:
293
+ value = event.attr_value.value
294
+ reading = Reading(
295
+ value=value,
296
+ timestamp=event.get_date().totime(),
297
+ alarm_severity=event.attr_value.quality,
298
+ )
299
+ if self._callback is not None:
300
+ self._callback(reading, value)
301
+
302
+ async def poll(self):
303
+ """
304
+ Poll the attribute and call the callback if the value has changed by more
305
+ than the absolute or relative change. This function is used when an attribute
306
+ that does not support events is cached or a callback is passed to it.
307
+ """
308
+ try:
309
+ last_reading = await self.get_reading()
310
+ flag = 0
311
+ # Initial reading
312
+ if self._callback is not None:
313
+ self._callback(last_reading, last_reading["value"])
314
+ except Exception as e:
315
+ raise RuntimeError(f"Could not poll the attribute: {e}") from e
316
+
317
+ try:
318
+ # If the value is a number, we can check for changes
319
+ if isinstance(last_reading["value"], int | float):
320
+ while True:
321
+ await asyncio.sleep(self._polling_period)
322
+ reading = await self.get_reading()
323
+ if reading is None or reading["value"] is None:
324
+ continue
325
+ diff = abs(reading["value"] - last_reading["value"])
326
+ if self._abs_change is not None and diff >= abs(self._abs_change):
327
+ if self._callback is not None:
328
+ self._callback(reading, reading["value"])
329
+ flag = 0
330
+
331
+ elif (
332
+ self._rel_change is not None
333
+ and diff >= self._rel_change * abs(last_reading["value"])
334
+ ):
335
+ if self._callback is not None:
336
+ self._callback(reading, reading["value"])
337
+ flag = 0
338
+
339
+ else:
340
+ flag = (flag + 1) % 4
341
+ if flag == 0 and self._callback is not None:
342
+ self._callback(reading, reading["value"])
343
+
344
+ last_reading = reading.copy()
345
+ if self._callback is None:
346
+ break
347
+ # If the value is not a number, we can only poll
348
+ else:
349
+ while True:
350
+ await asyncio.sleep(self._polling_period)
351
+ flag = (flag + 1) % 4
352
+ if flag == 0:
353
+ reading = await self.get_reading()
354
+ if reading is None or reading["value"] is None:
355
+ continue
356
+ if isinstance(reading["value"], np.ndarray):
357
+ if not np.array_equal(
358
+ reading["value"], last_reading["value"]
359
+ ):
360
+ if self._callback is not None:
361
+ self._callback(reading, reading["value"])
362
+ else:
363
+ break
364
+ else:
365
+ if reading["value"] != last_reading["value"]:
366
+ if self._callback is not None:
367
+ self._callback(reading, reading["value"])
368
+ else:
369
+ break
370
+ last_reading = reading.copy()
371
+ except Exception as e:
372
+ raise RuntimeError(f"Could not poll the attribute: {e}") from e
373
+
374
+ def set_polling(
375
+ self,
376
+ allow_polling: bool = False,
377
+ polling_period: float = 0.5,
378
+ abs_change: float | None = None,
379
+ rel_change: float | None = 0.1,
380
+ ):
381
+ """
382
+ Set the polling parameters.
383
+ """
384
+ self._allow_polling = allow_polling
385
+ self._polling_period = polling_period
386
+ self._abs_change = abs_change
387
+ self._rel_change = rel_change
388
+
389
+
390
+ class CommandProxy(TangoProxy):
391
+ _last_reading: Reading = Reading(value=None, timestamp=0, alarm_severity=0)
392
+
393
+ def subscribe_callback(self, callback: ReadingValueCallback | None) -> None:
394
+ raise NotImplementedError("Cannot subscribe to commands")
395
+
396
+ def unsubscribe_callback(self) -> None:
397
+ raise NotImplementedError("Cannot unsubscribe from commands")
398
+
399
+ async def get(self) -> object:
400
+ return self._last_reading["value"]
401
+
402
+ async def get_w_value(self) -> object:
403
+ return self._last_reading["value"]
404
+
405
+ async def connect(self) -> None:
406
+ pass
407
+
408
+ @ensure_proper_executor
409
+ async def put(
410
+ self, value: object | None, wait: bool = True, timeout: float | None = None
411
+ ) -> AsyncStatus | None:
412
+ if wait:
413
+ try:
414
+
415
+ async def _put():
416
+ return await self._proxy.command_inout(self._name, value)
417
+
418
+ task = asyncio.create_task(_put())
419
+ val = await asyncio.wait_for(task, timeout)
420
+ self._last_reading = Reading(
421
+ value=val, timestamp=time.time(), alarm_severity=0
422
+ )
423
+ except asyncio.TimeoutError as te:
424
+ raise TimeoutError(f"{self._name} command failed: Timeout") from te
425
+ except DevFailed as de:
426
+ raise RuntimeError(
427
+ f"{self._name} device" f" failure: {de.args[0].desc}"
428
+ ) from de
429
+
430
+ else:
431
+ rid = self._proxy.command_inout_asynch(self._name, value)
432
+
433
+ async def wait_for_reply(rd: int, to: float | None):
434
+ start_time = time.time()
435
+ while True:
436
+ try:
437
+ reply_value = self._proxy.command_inout_reply(rd)
438
+ self._last_reading = Reading(
439
+ value=reply_value, timestamp=time.time(), alarm_severity=0
440
+ )
441
+ break
442
+ except DevFailed as de_exc:
443
+ if de_exc.args[0].reason == "API_AsynReplyNotArrived":
444
+ await asyncio.sleep(A_BIT)
445
+ if to and time.time() - start_time > to:
446
+ raise TimeoutError(
447
+ "Timeout while waiting for command reply"
448
+ ) from de_exc
449
+ else:
450
+ raise RuntimeError(
451
+ f"{self._name} device failure:"
452
+ f" {de_exc.args[0].desc}"
453
+ ) from de_exc
454
+
455
+ return AsyncStatus(wait_for_reply(rid, timeout))
456
+
457
+ @ensure_proper_executor
458
+ async def get_config(self) -> CommandInfo:
459
+ return await self._proxy.get_command_config(self._name)
460
+
461
+ async def get_reading(self) -> Reading:
462
+ reading = Reading(
463
+ value=self._last_reading["value"],
464
+ timestamp=self._last_reading["timestamp"],
465
+ alarm_severity=self._last_reading.get("alarm_severity", 0),
466
+ )
467
+ return reading
468
+
469
+ def set_polling(
470
+ self,
471
+ allow_polling: bool = False,
472
+ polling_period: float = 0.5,
473
+ abs_change: float | None = None,
474
+ rel_change: float | None = 0.1,
475
+ ):
476
+ pass
477
+
478
+
479
+ def get_dtype_extended(datatype) -> object | None:
480
+ # DevState tango type does not have numpy equivalents
481
+ dtype = get_dtype(datatype)
482
+ if dtype == np.object_:
483
+ if datatype.__args__[1].__args__[0] == DevState:
484
+ dtype = CmdArgType.DevState
485
+ return dtype
486
+
487
+
488
+ def get_trl_descriptor(
489
+ datatype: type | None,
490
+ tango_resource: str,
491
+ tr_configs: dict[str, AttributeInfoEx | CommandInfo],
492
+ ) -> Descriptor:
493
+ tr_dtype = {}
494
+ for tr_name, config in tr_configs.items():
495
+ if isinstance(config, AttributeInfoEx):
496
+ _, dtype, descr = get_python_type(config.data_type)
497
+ tr_dtype[tr_name] = config.data_format, dtype, descr
498
+ elif isinstance(config, CommandInfo):
499
+ if (
500
+ config.in_type != CmdArgType.DevVoid
501
+ and config.out_type != CmdArgType.DevVoid
502
+ and config.in_type != config.out_type
503
+ ):
504
+ raise RuntimeError(
505
+ "Commands with different in and out dtypes are not supported"
506
+ )
507
+ array, dtype, descr = get_python_type(
508
+ config.in_type
509
+ if config.in_type != CmdArgType.DevVoid
510
+ else config.out_type
511
+ )
512
+ tr_dtype[tr_name] = (
513
+ AttrDataFormat.SPECTRUM if array else AttrDataFormat.SCALAR,
514
+ dtype,
515
+ descr,
516
+ )
517
+ else:
518
+ raise RuntimeError(f"Unknown config type: {type(config)}")
519
+ tr_format, tr_dtype, tr_dtype_desc = get_unique(tr_dtype, "typeids")
520
+
521
+ # tango commands are limited in functionality:
522
+ # they do not have info about shape and Enum labels
523
+ trl_config = list(tr_configs.values())[0]
524
+ max_x: int = (
525
+ trl_config.max_dim_x
526
+ if hasattr(trl_config, "max_dim_x")
527
+ else np.iinfo(np.int32).max
528
+ )
529
+ max_y: int = (
530
+ trl_config.max_dim_y
531
+ if hasattr(trl_config, "max_dim_y")
532
+ else np.iinfo(np.int32).max
533
+ )
534
+ # is_attr = hasattr(trl_config, "enum_labels")
535
+ # trl_choices = list(trl_config.enum_labels) if is_attr else []
536
+
537
+ if tr_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]:
538
+ # This is an array
539
+ if datatype:
540
+ # Check we wanted an array of this type
541
+ dtype = get_dtype_extended(datatype)
542
+ if not dtype:
543
+ raise TypeError(
544
+ f"{tango_resource} has type [{tr_dtype}] not {datatype.__name__}"
545
+ )
546
+ if dtype != tr_dtype:
547
+ raise TypeError(f"{tango_resource} has type [{tr_dtype}] not [{dtype}]")
548
+
549
+ if tr_format == AttrDataFormat.SPECTRUM:
550
+ return Descriptor(source=tango_resource, dtype="array", shape=[max_x])
551
+ elif tr_format == AttrDataFormat.IMAGE:
552
+ return Descriptor(
553
+ source=tango_resource, dtype="array", shape=[max_y, max_x]
554
+ )
555
+
556
+ else:
557
+ if tr_dtype in (Enum, CmdArgType.DevState):
558
+ # if tr_dtype == CmdArgType.DevState:
559
+ # trl_choices = list(DevState.names.keys())
560
+
561
+ if datatype:
562
+ if not issubclass(datatype, Enum | DevState):
563
+ raise TypeError(
564
+ f"{tango_resource} has type Enum not {datatype.__name__}"
565
+ )
566
+ # if tr_dtype == Enum and is_attr:
567
+ # if isinstance(datatype, DevState):
568
+ # choices = tuple(v.name for v in datatype)
569
+ # if set(choices) != set(trl_choices):
570
+ # raise TypeError(
571
+ # f"{tango_resource} has choices {trl_choices} "
572
+ # f"not {choices}"
573
+ # )
574
+ return Descriptor(source=tango_resource, dtype="string", shape=[])
575
+ else:
576
+ if datatype and not issubclass(tr_dtype, datatype):
577
+ raise TypeError(
578
+ f"{tango_resource} has type {tr_dtype.__name__} "
579
+ f"not {datatype.__name__}"
580
+ )
581
+ return Descriptor(source=tango_resource, dtype=tr_dtype_desc, shape=[])
582
+
583
+ raise RuntimeError(f"Error getting descriptor for {tango_resource}")
584
+
585
+
586
+ async def get_tango_trl(
587
+ full_trl: str, device_proxy: DeviceProxy | TangoProxy | None
588
+ ) -> TangoProxy:
589
+ if isinstance(device_proxy, TangoProxy):
590
+ return device_proxy
591
+ device_trl, trl_name = full_trl.rsplit("/", 1)
592
+ trl_name = trl_name.lower()
593
+ if device_proxy is None:
594
+ device_proxy = await AsyncDeviceProxy(device_trl)
595
+
596
+ # all attributes can be always accessible with low register
597
+ if isinstance(device_proxy, DeviceProxy):
598
+ all_attrs = [
599
+ attr_name.lower() for attr_name in device_proxy.get_attribute_list()
600
+ ]
601
+ else:
602
+ raise TypeError(
603
+ f"device_proxy must be an instance of DeviceProxy for {full_trl}"
604
+ )
605
+ if trl_name in all_attrs:
606
+ return AttributeProxy(device_proxy, trl_name)
607
+
608
+ # all commands can be always accessible with low register
609
+ all_cmds = [cmd_name.lower() for cmd_name in device_proxy.get_command_list()]
610
+ if trl_name in all_cmds:
611
+ return CommandProxy(device_proxy, trl_name)
612
+
613
+ # If version is below tango 9, then pipes are not supported
614
+ if device_proxy.info().server_version >= 9:
615
+ # all pipes can be always accessible with low register
616
+ all_pipes = [pipe_name.lower() for pipe_name in device_proxy.get_pipe_list()]
617
+ if trl_name in all_pipes:
618
+ raise NotImplementedError("Pipes are not supported")
619
+
620
+ raise RuntimeError(f"{trl_name} cannot be found in {device_proxy.name()}")
621
+
622
+
623
+ class TangoSignalBackend(SignalBackend[T]):
624
+ def __init__(
625
+ self,
626
+ datatype: type[T] | None,
627
+ read_trl: str = "",
628
+ write_trl: str = "",
629
+ device_proxy: DeviceProxy | None = None,
630
+ ):
631
+ self.device_proxy = device_proxy
632
+ self.datatype = datatype
633
+ self.read_trl = read_trl
634
+ self.write_trl = write_trl
635
+ self.proxies: dict[str, TangoProxy | DeviceProxy | None] = {
636
+ read_trl: self.device_proxy,
637
+ write_trl: self.device_proxy,
638
+ }
639
+ self.trl_configs: dict[str, AttributeInfoEx] = {}
640
+ self.descriptor: Descriptor = {} # type: ignore
641
+ self._polling: tuple[bool, float, float | None, float | None] = (
642
+ False,
643
+ 0.1,
644
+ None,
645
+ 0.1,
646
+ )
647
+ self.support_events: bool = True
648
+ self.status: AsyncStatus | None = None
649
+
650
+ @classmethod
651
+ def datatype_allowed(cls, dtype: Any) -> bool:
652
+ return dtype in (int, float, str, bool, np.ndarray, Enum, DevState)
653
+
654
+ def set_trl(self, read_trl: str = "", write_trl: str = ""):
655
+ self.read_trl = read_trl
656
+ self.write_trl = write_trl if write_trl else read_trl
657
+ self.proxies = {
658
+ read_trl: self.device_proxy,
659
+ write_trl: self.device_proxy,
660
+ }
661
+
662
+ def source(self, name: str) -> str:
663
+ return self.read_trl
664
+
665
+ async def _connect_and_store_config(self, trl: str) -> None:
666
+ if not trl:
667
+ raise RuntimeError(f"trl not set for {self}")
668
+ try:
669
+ self.proxies[trl] = await get_tango_trl(trl, self.proxies[trl])
670
+ if self.proxies[trl] is None:
671
+ raise NotConnected(f"Not connected to {trl}")
672
+ # Pyright does not believe that self.proxies[trl] is not None despite
673
+ # the check above
674
+ await self.proxies[trl].connect() # type: ignore
675
+ self.trl_configs[trl] = await self.proxies[trl].get_config() # type: ignore
676
+ self.proxies[trl].support_events = self.support_events # type: ignore
677
+ except CancelledError as ce:
678
+ raise NotConnected(f"Could not connect to {trl}") from ce
679
+
680
+ async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
681
+ if not self.read_trl:
682
+ raise RuntimeError(f"trl not set for {self}")
683
+ if self.read_trl != self.write_trl:
684
+ # Different, need to connect both
685
+ await wait_for_connection(
686
+ read_trl=self._connect_and_store_config(self.read_trl),
687
+ write_trl=self._connect_and_store_config(self.write_trl),
688
+ )
689
+ else:
690
+ # The same, so only need to connect one
691
+ await self._connect_and_store_config(self.read_trl)
692
+ self.proxies[self.read_trl].set_polling(*self._polling) # type: ignore
693
+ self.descriptor = get_trl_descriptor(
694
+ self.datatype, self.read_trl, self.trl_configs
695
+ )
696
+
697
+ async def put(self, value: T | None, wait=True, timeout=None) -> None:
698
+ if self.proxies[self.write_trl] is None:
699
+ raise NotConnected(f"Not connected to {self.write_trl}")
700
+ self.status = None
701
+ put_status = await self.proxies[self.write_trl].put(value, wait, timeout) # type: ignore
702
+ self.status = put_status
703
+
704
+ async def get_datakey(self, source: str) -> Descriptor:
705
+ return self.descriptor
706
+
707
+ async def get_reading(self) -> Reading:
708
+ if self.proxies[self.read_trl] is None:
709
+ raise NotConnected(f"Not connected to {self.read_trl}")
710
+ return await self.proxies[self.read_trl].get_reading() # type: ignore
711
+
712
+ async def get_value(self) -> T:
713
+ if self.proxies[self.read_trl] is None:
714
+ raise NotConnected(f"Not connected to {self.read_trl}")
715
+ proxy = self.proxies[self.read_trl]
716
+ if proxy is None:
717
+ raise NotConnected(f"Not connected to {self.read_trl}")
718
+ return cast(T, await proxy.get())
719
+
720
+ async def get_setpoint(self) -> T:
721
+ if self.proxies[self.write_trl] is None:
722
+ raise NotConnected(f"Not connected to {self.write_trl}")
723
+ proxy = self.proxies[self.write_trl]
724
+ if proxy is None:
725
+ raise NotConnected(f"Not connected to {self.write_trl}")
726
+ return cast(T, await proxy.get_w_value())
727
+
728
+ def set_callback(self, callback: ReadingValueCallback | None) -> None:
729
+ if self.proxies[self.read_trl] is None:
730
+ raise NotConnected(f"Not connected to {self.read_trl}")
731
+ if self.support_events is False and self._polling[0] is False:
732
+ raise RuntimeError(
733
+ f"Cannot set event for {self.read_trl}. "
734
+ "Cannot set a callback on an attribute that does not support events and"
735
+ " for which polling is disabled."
736
+ )
737
+
738
+ if callback:
739
+ try:
740
+ assert not self.proxies[self.read_trl].has_subscription() # type: ignore
741
+ self.proxies[self.read_trl].subscribe_callback(callback) # type: ignore
742
+ except AssertionError as ae:
743
+ raise RuntimeError(
744
+ "Cannot set a callback when one" " is already set"
745
+ ) from ae
746
+ except RuntimeError as exc:
747
+ raise RuntimeError(
748
+ f"Cannot set callback" f" for {self.read_trl}. {exc}"
749
+ ) from exc
750
+
751
+ else:
752
+ self.proxies[self.read_trl].unsubscribe_callback() # type: ignore
753
+
754
+ def set_polling(
755
+ self,
756
+ allow_polling: bool = True,
757
+ polling_period: float = 0.1,
758
+ abs_change: float | None = None,
759
+ rel_change: float | None = 0.1,
760
+ ):
761
+ self._polling = (allow_polling, polling_period, abs_change, rel_change)
762
+
763
+ def allow_events(self, allow: bool = True):
764
+ self.support_events = allow