ophyd-async 0.7.0a1__py3-none-any.whl → 0.8.0a3__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 (83) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +30 -9
  3. ophyd_async/core/_detector.py +5 -10
  4. ophyd_async/core/_device.py +146 -67
  5. ophyd_async/core/_device_filler.py +269 -0
  6. ophyd_async/core/_device_save_loader.py +6 -7
  7. ophyd_async/core/_mock_signal_backend.py +32 -40
  8. ophyd_async/core/_mock_signal_utils.py +22 -16
  9. ophyd_async/core/_protocol.py +28 -8
  10. ophyd_async/core/_readable.py +133 -134
  11. ophyd_async/core/_signal.py +140 -152
  12. ophyd_async/core/_signal_backend.py +131 -64
  13. ophyd_async/core/_soft_signal_backend.py +125 -194
  14. ophyd_async/core/_status.py +22 -6
  15. ophyd_async/core/_table.py +97 -100
  16. ophyd_async/core/_utils.py +79 -18
  17. ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
  18. ophyd_async/epics/adaravis/_aravis_io.py +8 -6
  19. ophyd_async/epics/adcore/_core_io.py +5 -7
  20. ophyd_async/epics/adcore/_hdf_writer.py +2 -2
  21. ophyd_async/epics/adcore/_single_trigger.py +4 -9
  22. ophyd_async/epics/adcore/_utils.py +15 -10
  23. ophyd_async/epics/adkinetix/__init__.py +2 -1
  24. ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
  25. ophyd_async/epics/adkinetix/_kinetix_io.py +4 -5
  26. ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
  27. ophyd_async/epics/adpilatus/_pilatus_io.py +3 -4
  28. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  29. ophyd_async/epics/advimba/__init__.py +4 -1
  30. ophyd_async/epics/advimba/_vimba_controller.py +6 -3
  31. ophyd_async/epics/advimba/_vimba_io.py +8 -9
  32. ophyd_async/epics/core/__init__.py +26 -0
  33. ophyd_async/epics/core/_aioca.py +323 -0
  34. ophyd_async/epics/core/_epics_connector.py +53 -0
  35. ophyd_async/epics/core/_epics_device.py +13 -0
  36. ophyd_async/epics/core/_p4p.py +382 -0
  37. ophyd_async/epics/core/_pvi_connector.py +92 -0
  38. ophyd_async/epics/core/_signal.py +171 -0
  39. ophyd_async/epics/core/_util.py +61 -0
  40. ophyd_async/epics/demo/_mover.py +4 -5
  41. ophyd_async/epics/demo/_sensor.py +14 -13
  42. ophyd_async/epics/eiger/_eiger.py +1 -2
  43. ophyd_async/epics/eiger/_eiger_controller.py +1 -1
  44. ophyd_async/epics/eiger/_eiger_io.py +3 -5
  45. ophyd_async/epics/eiger/_odin_io.py +5 -5
  46. ophyd_async/epics/motor.py +4 -5
  47. ophyd_async/epics/signal.py +11 -0
  48. ophyd_async/fastcs/core.py +9 -0
  49. ophyd_async/fastcs/panda/__init__.py +4 -4
  50. ophyd_async/fastcs/panda/_block.py +23 -11
  51. ophyd_async/fastcs/panda/_control.py +3 -5
  52. ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
  53. ophyd_async/fastcs/panda/_table.py +29 -51
  54. ophyd_async/fastcs/panda/_trigger.py +8 -8
  55. ophyd_async/fastcs/panda/_writer.py +4 -7
  56. ophyd_async/plan_stubs/_ensure_connected.py +3 -1
  57. ophyd_async/plan_stubs/_fly.py +2 -2
  58. ophyd_async/plan_stubs/_nd_attributes.py +5 -4
  59. ophyd_async/py.typed +0 -0
  60. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
  61. ophyd_async/sim/demo/_sim_motor.py +3 -4
  62. ophyd_async/tango/__init__.py +2 -4
  63. ophyd_async/tango/base_devices/_base_device.py +76 -144
  64. ophyd_async/tango/demo/_counter.py +8 -18
  65. ophyd_async/tango/demo/_mover.py +5 -6
  66. ophyd_async/tango/signal/__init__.py +2 -4
  67. ophyd_async/tango/signal/_signal.py +29 -50
  68. ophyd_async/tango/signal/_tango_transport.py +38 -40
  69. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/METADATA +8 -12
  70. ophyd_async-0.8.0a3.dist-info/RECORD +112 -0
  71. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/WHEEL +1 -1
  72. ophyd_async/epics/pvi/__init__.py +0 -3
  73. ophyd_async/epics/pvi/_pvi.py +0 -338
  74. ophyd_async/epics/signal/__init__.py +0 -21
  75. ophyd_async/epics/signal/_aioca.py +0 -378
  76. ophyd_async/epics/signal/_common.py +0 -57
  77. ophyd_async/epics/signal/_epics_transport.py +0 -34
  78. ophyd_async/epics/signal/_p4p.py +0 -518
  79. ophyd_async/epics/signal/_signal.py +0 -114
  80. ophyd_async-0.7.0a1.dist-info/RECORD +0 -108
  81. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/LICENSE +0 -0
  82. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/entry_points.txt +0 -0
  83. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,11 @@
1
1
  import asyncio
2
2
 
3
- from ophyd_async.core import DetectorController, DetectorTrigger
4
- from ophyd_async.core._detector import TriggerInfo
5
- from ophyd_async.core._status import AsyncStatus
3
+ from ophyd_async.core import (
4
+ AsyncStatus,
5
+ DetectorController,
6
+ DetectorTrigger,
7
+ TriggerInfo,
8
+ )
6
9
  from ophyd_async.epics import adcore
7
10
 
8
11
  from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
@@ -1,10 +1,9 @@
1
- from enum import Enum
2
-
1
+ from ophyd_async.core import StrictEnum
3
2
  from ophyd_async.epics import adcore
4
- from ophyd_async.epics.signal import epics_signal_rw_rbv
3
+ from ophyd_async.epics.core import epics_signal_rw_rbv
5
4
 
6
5
 
7
- class VimbaPixelFormat(str, Enum):
6
+ class VimbaPixelFormat(StrictEnum):
8
7
  internal = "Mono8"
9
8
  ext_enable = "Mono12"
10
9
  ext_trigger = "Ext. Trigger"
@@ -12,7 +11,7 @@ class VimbaPixelFormat(str, Enum):
12
11
  alignment = "Alignment"
13
12
 
14
13
 
15
- class VimbaConvertFormat(str, Enum):
14
+ class VimbaConvertFormat(StrictEnum):
16
15
  none = "None"
17
16
  mono8 = "Mono8"
18
17
  mono16 = "Mono16"
@@ -20,7 +19,7 @@ class VimbaConvertFormat(str, Enum):
20
19
  rgb16 = "RGB16"
21
20
 
22
21
 
23
- class VimbaTriggerSource(str, Enum):
22
+ class VimbaTriggerSource(StrictEnum):
24
23
  freerun = "Freerun"
25
24
  line1 = "Line1"
26
25
  line2 = "Line2"
@@ -30,17 +29,17 @@ class VimbaTriggerSource(str, Enum):
30
29
  action1 = "Action1"
31
30
 
32
31
 
33
- class VimbaOverlap(str, Enum):
32
+ class VimbaOverlap(StrictEnum):
34
33
  off = "Off"
35
34
  prev_frame = "PreviousFrame"
36
35
 
37
36
 
38
- class VimbaOnOff(str, Enum):
37
+ class VimbaOnOff(StrictEnum):
39
38
  on = "On"
40
39
  off = "Off"
41
40
 
42
41
 
43
- class VimbaExposeOutMode(str, Enum):
42
+ class VimbaExposeOutMode(StrictEnum):
44
43
  timed = "Timed" # Use ExposureTime PV
45
44
  trigger_width = "TriggerWidth" # Expose for length of high signal
46
45
 
@@ -0,0 +1,26 @@
1
+ from ._epics_connector import EpicsDeviceConnector, PvSuffix
2
+ from ._epics_device import EpicsDevice
3
+ from ._pvi_connector import PviDeviceConnector
4
+ from ._signal import (
5
+ CaSignalBackend,
6
+ PvaSignalBackend,
7
+ epics_signal_r,
8
+ epics_signal_rw,
9
+ epics_signal_rw_rbv,
10
+ epics_signal_w,
11
+ epics_signal_x,
12
+ )
13
+
14
+ __all__ = [
15
+ "PviDeviceConnector",
16
+ "EpicsDeviceConnector",
17
+ "PvSuffix",
18
+ "EpicsDevice",
19
+ "CaSignalBackend",
20
+ "PvaSignalBackend",
21
+ "epics_signal_r",
22
+ "epics_signal_rw",
23
+ "epics_signal_rw_rbv",
24
+ "epics_signal_w",
25
+ "epics_signal_x",
26
+ ]
@@ -0,0 +1,323 @@
1
+ import logging
2
+ import sys
3
+ from collections.abc import Sequence
4
+ from math import isnan, nan
5
+ from typing import Any, Generic, cast
6
+
7
+ import numpy as np
8
+ from aioca import (
9
+ FORMAT_CTRL,
10
+ FORMAT_RAW,
11
+ FORMAT_TIME,
12
+ CANothing,
13
+ Subscription,
14
+ caget,
15
+ camonitor,
16
+ caput,
17
+ )
18
+ from aioca.types import AugmentedValue, Dbr, Format
19
+ from bluesky.protocols import Reading
20
+ from epicscorelibs.ca import dbr
21
+ from event_model import DataKey, Limits, LimitsRange
22
+
23
+ from ophyd_async.core import (
24
+ Array1D,
25
+ Callback,
26
+ NotConnected,
27
+ SignalDatatype,
28
+ SignalDatatypeT,
29
+ SignalMetadata,
30
+ get_enum_cls,
31
+ get_unique,
32
+ make_datakey,
33
+ wait_for_connection,
34
+ )
35
+
36
+ from ._util import EpicsSignalBackend, format_datatype, get_supported_values
37
+
38
+
39
+ def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
40
+ def get_limits(limit: str) -> LimitsRange | None:
41
+ low = getattr(value, f"lower_{limit}_limit", nan)
42
+ high = getattr(value, f"upper_{limit}_limit", nan)
43
+ if not (isnan(low) and isnan(high)):
44
+ return LimitsRange(
45
+ low=None if isnan(low) else low,
46
+ high=None if isnan(high) else high,
47
+ )
48
+
49
+ limits = Limits()
50
+ if limits_range := get_limits("alarm"):
51
+ limits["alarm"] = limits_range
52
+ if limits_range := get_limits("ctrl"):
53
+ limits["control"] = limits_range
54
+ if limits_range := get_limits("disp"):
55
+ limits["display"] = limits_range
56
+ if limits_range := get_limits("warning"):
57
+ limits["warning"] = limits_range
58
+ return limits
59
+
60
+
61
+ def _metadata_from_augmented_value(
62
+ value: AugmentedValue, metadata: SignalMetadata
63
+ ) -> SignalMetadata:
64
+ metadata = metadata.copy()
65
+ if hasattr(value, "units"):
66
+ metadata["units"] = value.units
67
+ if hasattr(value, "precision") and not isnan(value.precision):
68
+ metadata["precision"] = value.precision
69
+ if limits := _limits_from_augmented_value(value):
70
+ metadata["limits"] = limits
71
+ return metadata
72
+
73
+
74
+ class CaConverter(Generic[SignalDatatypeT]):
75
+ def __init__(
76
+ self,
77
+ datatype: type[SignalDatatypeT],
78
+ read_dbr: Dbr,
79
+ write_dbr: Dbr | None = None,
80
+ metadata: SignalMetadata | None = None,
81
+ ):
82
+ self.datatype = datatype
83
+ self.read_dbr: Dbr = read_dbr
84
+ self.write_dbr: Dbr | None = write_dbr
85
+ self.metadata = metadata or SignalMetadata()
86
+
87
+ def write_value(self, value: Any) -> Any:
88
+ # The ca library will do the conversion for us
89
+ return value
90
+
91
+ def value(self, value: AugmentedValue) -> SignalDatatypeT:
92
+ # for channel access ca_xxx classes, this
93
+ # invokes __pos__ operator to return an instance of
94
+ # the builtin base class
95
+ return +value # type: ignore
96
+
97
+
98
+ class DisconnectedCaConverter(CaConverter):
99
+ def __getattribute__(self, __name: str) -> Any:
100
+ raise NotImplementedError("No PV has been set as connect() has not been called")
101
+
102
+
103
+ class CaArrayConverter(CaConverter[np.ndarray]):
104
+ def value(self, value: AugmentedValue) -> np.ndarray:
105
+ # A less expensive conversion
106
+ return np.array(value, copy=False)
107
+
108
+
109
+ class CaSequenceStrConverter(CaConverter[Sequence[str]]):
110
+ def value(self, value: AugmentedValue) -> Sequence[str]:
111
+ return [str(v) for v in value] # type: ignore
112
+
113
+
114
+ class CaLongStrConverter(CaConverter[str]):
115
+ def __init__(self):
116
+ super().__init__(str, dbr.DBR_CHAR_STR, dbr.DBR_CHAR_STR)
117
+
118
+ def write_value_and_dbr(self, value: Any) -> Any:
119
+ # Add a null in here as this is what the commandline caput does
120
+ # TODO: this should be in the server so check if it can be pushed to asyn
121
+ return value + "\0"
122
+
123
+
124
+ class CaBoolConverter(CaConverter[bool]):
125
+ def __init__(self):
126
+ super().__init__(bool, dbr.DBR_SHORT)
127
+
128
+ def value(self, value: AugmentedValue) -> bool:
129
+ return bool(value)
130
+
131
+
132
+ class CaEnumConverter(CaConverter[str]):
133
+ def __init__(self, supported_values: dict[str, str]):
134
+ self.supported_values = supported_values
135
+ super().__init__(
136
+ str, dbr.DBR_STRING, metadata=SignalMetadata(choices=list(supported_values))
137
+ )
138
+
139
+ def value(self, value: AugmentedValue) -> str:
140
+ return self.supported_values[str(value)]
141
+
142
+
143
+ _datatype_converter_from_dbr: dict[
144
+ tuple[Dbr, bool], tuple[type[SignalDatatype], type[CaConverter]]
145
+ ] = {
146
+ (dbr.DBR_STRING, False): (str, CaConverter),
147
+ (dbr.DBR_SHORT, False): (int, CaConverter),
148
+ (dbr.DBR_FLOAT, False): (float, CaConverter),
149
+ (dbr.DBR_ENUM, False): (str, CaConverter),
150
+ (dbr.DBR_CHAR, False): (int, CaConverter),
151
+ (dbr.DBR_LONG, False): (int, CaConverter),
152
+ (dbr.DBR_DOUBLE, False): (float, CaConverter),
153
+ (dbr.DBR_STRING, True): (Sequence[str], CaSequenceStrConverter),
154
+ (dbr.DBR_SHORT, True): (Array1D[np.int16], CaArrayConverter),
155
+ (dbr.DBR_FLOAT, True): (Array1D[np.float32], CaArrayConverter),
156
+ (dbr.DBR_ENUM, True): (Sequence[str], CaSequenceStrConverter),
157
+ (dbr.DBR_CHAR, True): (Array1D[np.uint8], CaArrayConverter),
158
+ (dbr.DBR_LONG, True): (Array1D[np.int32], CaArrayConverter),
159
+ (dbr.DBR_DOUBLE, True): (Array1D[np.float64], CaArrayConverter),
160
+ }
161
+
162
+
163
+ def make_converter(
164
+ datatype: type | None, values: dict[str, AugmentedValue]
165
+ ) -> CaConverter:
166
+ pv = list(values)[0]
167
+ pv_dbr = cast(
168
+ Dbr, get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
169
+ )
170
+ is_array = bool([v for v in values.values() if v.element_count > 1])
171
+ # Infer a datatype and converter from the dbr
172
+ inferred_datatype, converter_cls = _datatype_converter_from_dbr[(pv_dbr, is_array)]
173
+ # Some override cases
174
+ if is_array and pv_dbr == dbr.DBR_CHAR and datatype is str:
175
+ # Override waveform of chars to be treated as string
176
+ return CaLongStrConverter()
177
+ elif not is_array and datatype is bool and pv_dbr == dbr.DBR_ENUM:
178
+ # Database can't do bools, so are often representated as enums of len 2
179
+ pv_num_choices = get_unique(
180
+ {k: len(v.enums) for k, v in values.items()}, "number of choices"
181
+ )
182
+ if pv_num_choices != 2:
183
+ raise TypeError(f"{pv} has {pv_num_choices} choices, can't map to bool")
184
+ return CaBoolConverter()
185
+ elif not is_array and pv_dbr == dbr.DBR_ENUM:
186
+ pv_choices = get_unique(
187
+ {k: tuple(v.enums) for k, v in values.items()}, "choices"
188
+ )
189
+ if enum_cls := get_enum_cls(datatype):
190
+ # If explicitly requested then check
191
+ return CaEnumConverter(get_supported_values(pv, enum_cls, pv_choices))
192
+ elif datatype in (None, str):
193
+ # Drop to string for safety, but retain choices as metadata
194
+ return CaConverter(
195
+ str,
196
+ dbr.DBR_STRING,
197
+ metadata=SignalMetadata(choices=list(pv_choices)),
198
+ )
199
+ elif (
200
+ inferred_datatype is float
201
+ and datatype is int
202
+ and get_unique({k: v.precision for k, v in values.items()}, "precision") == 0
203
+ ):
204
+ # Allow int signals to represent float records when prec is 0
205
+ return CaConverter(int, pv_dbr)
206
+ elif datatype in (None, inferred_datatype):
207
+ # If datatype matches what we are given then allow it and use inferred converter
208
+ return converter_cls(inferred_datatype, pv_dbr)
209
+ if pv_dbr == dbr.DBR_ENUM:
210
+ inferred_datatype = "str | SubsetEnum | StrictEnum"
211
+ raise TypeError(
212
+ f"{pv} with inferred datatype {format_datatype(inferred_datatype)}"
213
+ f" cannot be coerced to {format_datatype(datatype)}"
214
+ )
215
+
216
+
217
+ _tried_pyepics = False
218
+
219
+
220
+ def _use_pyepics_context_if_imported():
221
+ global _tried_pyepics
222
+ if not _tried_pyepics:
223
+ ca = sys.modules.get("epics.ca", None)
224
+ if ca:
225
+ ca.use_initial_context()
226
+ _tried_pyepics = True
227
+
228
+
229
+ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
230
+ def __init__(
231
+ self,
232
+ datatype: type[SignalDatatypeT] | None,
233
+ read_pv: str = "",
234
+ write_pv: str = "",
235
+ ):
236
+ self.converter: CaConverter = DisconnectedCaConverter(float, dbr.DBR_DOUBLE)
237
+ self.initial_values: dict[str, AugmentedValue] = {}
238
+ self.subscription: Subscription | None = None
239
+ super().__init__(datatype, read_pv, write_pv)
240
+
241
+ def source(self, name: str, read: bool):
242
+ return f"ca://{self.read_pv if read else self.write_pv}"
243
+
244
+ async def _store_initial_value(self, pv: str, timeout: float):
245
+ try:
246
+ self.initial_values[pv] = await caget(
247
+ pv, format=FORMAT_CTRL, timeout=timeout
248
+ )
249
+ except CANothing as exc:
250
+ logging.debug(f"signal ca://{pv} timed out")
251
+ raise NotConnected(f"ca://{pv}") from exc
252
+
253
+ async def connect(self, timeout: float):
254
+ _use_pyepics_context_if_imported()
255
+ if self.read_pv != self.write_pv:
256
+ # Different, need to connect both
257
+ await wait_for_connection(
258
+ read_pv=self._store_initial_value(self.read_pv, timeout=timeout),
259
+ write_pv=self._store_initial_value(self.write_pv, timeout=timeout),
260
+ )
261
+ else:
262
+ # The same, so only need to connect one
263
+ await self._store_initial_value(self.read_pv, timeout=timeout)
264
+ self.converter = make_converter(self.datatype, self.initial_values)
265
+
266
+ async def _caget(self, pv: str, format: Format) -> AugmentedValue:
267
+ return await caget(
268
+ pv, datatype=self.converter.read_dbr, format=format, timeout=None
269
+ )
270
+
271
+ def _make_reading(self, value: AugmentedValue) -> Reading[SignalDatatypeT]:
272
+ return {
273
+ "value": self.converter.value(value),
274
+ "timestamp": value.timestamp,
275
+ "alarm_severity": -1 if value.severity > 2 else value.severity,
276
+ }
277
+
278
+ async def put(self, value: SignalDatatypeT | None, wait: bool):
279
+ if value is None:
280
+ write_value = self.initial_values[self.write_pv]
281
+ else:
282
+ write_value = self.converter.write_value(value)
283
+ await caput(
284
+ self.write_pv,
285
+ write_value,
286
+ datatype=self.converter.write_dbr,
287
+ wait=wait,
288
+ timeout=None,
289
+ )
290
+
291
+ async def get_datakey(self, source: str) -> DataKey:
292
+ value = await self._caget(self.read_pv, FORMAT_CTRL)
293
+ metadata = _metadata_from_augmented_value(value, self.converter.metadata)
294
+ return make_datakey(
295
+ self.converter.datatype, self.converter.value(value), source, metadata
296
+ )
297
+
298
+ async def get_reading(self) -> Reading[SignalDatatypeT]:
299
+ value = await self._caget(self.read_pv, FORMAT_TIME)
300
+ return self._make_reading(value)
301
+
302
+ async def get_value(self) -> SignalDatatypeT:
303
+ value = await self._caget(self.read_pv, FORMAT_RAW)
304
+ return self.converter.value(value)
305
+
306
+ async def get_setpoint(self) -> SignalDatatypeT:
307
+ value = await self._caget(self.write_pv, FORMAT_RAW)
308
+ return self.converter.value(value)
309
+
310
+ def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
311
+ if callback:
312
+ assert (
313
+ not self.subscription
314
+ ), "Cannot set a callback when one is already set"
315
+ self.subscription = camonitor(
316
+ self.read_pv,
317
+ lambda v: callback(self._make_reading(v)),
318
+ datatype=self.converter.read_dbr,
319
+ format=FORMAT_TIME,
320
+ )
321
+ elif self.subscription:
322
+ self.subscription.close()
323
+ self.subscription = None
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from ophyd_async.core import Device, DeviceConnector, DeviceFiller
7
+
8
+ from ._signal import EpicsSignalBackend, get_signal_backend_type, split_protocol_from_pv
9
+
10
+
11
+ @dataclass
12
+ class PvSuffix:
13
+ read_suffix: str
14
+ write_suffix: str | None = None
15
+
16
+ @classmethod
17
+ def rbv(cls, write_suffix: str, rbv_suffix: str = "_RBV") -> PvSuffix:
18
+ return cls(write_suffix + rbv_suffix, write_suffix)
19
+
20
+
21
+ def fill_backend_with_prefix(
22
+ prefix: str, backend: EpicsSignalBackend, annotations: list[Any]
23
+ ):
24
+ unhandled = []
25
+ while annotations:
26
+ annotation = annotations.pop(0)
27
+ if isinstance(annotation, PvSuffix):
28
+ backend.read_pv = prefix + annotation.read_suffix
29
+ backend.write_pv = prefix + (
30
+ annotation.write_suffix or annotation.read_suffix
31
+ )
32
+ else:
33
+ unhandled.append(annotation)
34
+ annotations.extend(unhandled)
35
+ # These leftover annotations will now be handled by the iterator
36
+
37
+
38
+ class EpicsDeviceConnector(DeviceConnector):
39
+ def __init__(self, prefix: str) -> None:
40
+ self.prefix = prefix
41
+
42
+ def create_children_from_annotations(self, device: Device):
43
+ if not hasattr(self, "filler"):
44
+ protocol, prefix = split_protocol_from_pv(self.prefix)
45
+ self.filler = DeviceFiller(
46
+ device,
47
+ signal_backend_factory=get_signal_backend_type(protocol),
48
+ device_connector_factory=DeviceConnector,
49
+ )
50
+ for backend, annotations in self.filler.create_signals_from_annotations():
51
+ fill_backend_with_prefix(prefix, backend, annotations)
52
+
53
+ list(self.filler.create_devices_from_annotations())
@@ -0,0 +1,13 @@
1
+ from ophyd_async.core import Device
2
+
3
+ from ._epics_connector import EpicsDeviceConnector
4
+ from ._pvi_connector import PviDeviceConnector
5
+
6
+
7
+ class EpicsDevice(Device):
8
+ def __init__(self, prefix: str, with_pvi: bool = False, name: str = ""):
9
+ if with_pvi:
10
+ connector = PviDeviceConnector(prefix)
11
+ else:
12
+ connector = EpicsDeviceConnector(prefix)
13
+ super().__init__(name=name, connector=connector)