ophyd-async 0.7.0__py3-none-any.whl → 0.8.0a2__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 (70) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +23 -8
  3. ophyd_async/core/_detector.py +5 -10
  4. ophyd_async/core/_device.py +139 -66
  5. ophyd_async/core/_device_filler.py +191 -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 +5 -5
  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 +71 -18
  17. ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
  18. ophyd_async/epics/adaravis/_aravis_io.py +7 -5
  19. ophyd_async/epics/adcore/_core_io.py +4 -6
  20. ophyd_async/epics/adcore/_hdf_writer.py +2 -2
  21. ophyd_async/epics/adcore/_utils.py +15 -10
  22. ophyd_async/epics/adkinetix/__init__.py +2 -1
  23. ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
  24. ophyd_async/epics/adkinetix/_kinetix_io.py +3 -4
  25. ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
  26. ophyd_async/epics/adpilatus/_pilatus_io.py +2 -3
  27. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  28. ophyd_async/epics/advimba/__init__.py +4 -1
  29. ophyd_async/epics/advimba/_vimba_controller.py +6 -3
  30. ophyd_async/epics/advimba/_vimba_io.py +7 -8
  31. ophyd_async/epics/demo/_sensor.py +8 -4
  32. ophyd_async/epics/eiger/_eiger.py +1 -2
  33. ophyd_async/epics/eiger/_eiger_controller.py +1 -1
  34. ophyd_async/epics/eiger/_eiger_io.py +2 -4
  35. ophyd_async/epics/eiger/_odin_io.py +4 -4
  36. ophyd_async/epics/pvi/__init__.py +2 -2
  37. ophyd_async/epics/pvi/_pvi.py +56 -321
  38. ophyd_async/epics/signal/__init__.py +3 -4
  39. ophyd_async/epics/signal/_aioca.py +184 -236
  40. ophyd_async/epics/signal/_common.py +35 -49
  41. ophyd_async/epics/signal/_p4p.py +254 -387
  42. ophyd_async/epics/signal/_signal.py +63 -21
  43. ophyd_async/fastcs/core.py +9 -0
  44. ophyd_async/fastcs/panda/__init__.py +4 -4
  45. ophyd_async/fastcs/panda/_block.py +18 -13
  46. ophyd_async/fastcs/panda/_control.py +3 -5
  47. ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
  48. ophyd_async/fastcs/panda/_table.py +29 -51
  49. ophyd_async/fastcs/panda/_trigger.py +8 -8
  50. ophyd_async/fastcs/panda/_writer.py +2 -5
  51. ophyd_async/plan_stubs/_ensure_connected.py +3 -1
  52. ophyd_async/plan_stubs/_fly.py +2 -2
  53. ophyd_async/plan_stubs/_nd_attributes.py +5 -4
  54. ophyd_async/py.typed +0 -0
  55. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
  56. ophyd_async/tango/__init__.py +2 -4
  57. ophyd_async/tango/base_devices/_base_device.py +76 -143
  58. ophyd_async/tango/demo/_counter.py +2 -2
  59. ophyd_async/tango/demo/_mover.py +2 -2
  60. ophyd_async/tango/signal/__init__.py +2 -4
  61. ophyd_async/tango/signal/_signal.py +29 -50
  62. ophyd_async/tango/signal/_tango_transport.py +38 -40
  63. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/METADATA +8 -12
  64. ophyd_async-0.8.0a2.dist-info/RECORD +110 -0
  65. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/WHEEL +1 -1
  66. ophyd_async/epics/signal/_epics_transport.py +0 -34
  67. ophyd_async-0.7.0.dist-info/RECORD +0 -108
  68. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/LICENSE +0 -0
  69. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/entry_points.txt +0 -0
  70. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,8 @@
1
- import inspect
2
1
  import logging
3
2
  import sys
4
3
  from collections.abc import Sequence
5
- from dataclasses import dataclass
6
- from enum import Enum
7
4
  from math import isnan, nan
8
- from typing import Any, get_origin
5
+ from typing import Any, Generic, cast
9
6
 
10
7
  import numpy as np
11
8
  from aioca import (
@@ -21,233 +18,201 @@ from aioca import (
21
18
  from aioca.types import AugmentedValue, Dbr, Format
22
19
  from bluesky.protocols import Reading
23
20
  from epicscorelibs.ca import dbr
24
- from event_model import DataKey
25
- from event_model.documents.event_descriptor import Dtype
21
+ from event_model import DataKey, Limits, LimitsRange
26
22
 
27
23
  from ophyd_async.core import (
28
- DEFAULT_TIMEOUT,
24
+ Array1D,
25
+ Callback,
29
26
  NotConnected,
30
- ReadingValueCallback,
31
- RuntimeSubsetEnum,
32
27
  SignalBackend,
33
- T,
34
- get_dtype,
28
+ SignalDatatype,
29
+ SignalDatatypeT,
30
+ SignalMetadata,
31
+ get_enum_cls,
35
32
  get_unique,
33
+ make_datakey,
36
34
  wait_for_connection,
37
35
  )
38
36
 
39
- from ._common import LimitPair, Limits, common_meta, get_supported_values
40
-
41
- dbr_to_dtype: dict[Dbr, Dtype] = {
42
- dbr.DBR_STRING: "string",
43
- dbr.DBR_SHORT: "integer",
44
- dbr.DBR_FLOAT: "number",
45
- dbr.DBR_CHAR: "string",
46
- dbr.DBR_LONG: "integer",
47
- dbr.DBR_DOUBLE: "number",
48
- }
49
-
50
-
51
- def _data_key_from_augmented_value(
52
- value: AugmentedValue,
53
- *,
54
- choices: list[str] | None = None,
55
- dtype: Dtype | None = None,
56
- ) -> DataKey:
57
- """Use the return value of get with FORMAT_CTRL to construct a DataKey
58
- describing the signal. See docstring of AugmentedValue for expected
59
- value fields by DBR type.
60
-
61
- Args:
62
- value (AugmentedValue): Description of the the return type of a DB record
63
- choices: Optional list of enum choices to pass as metadata in the datakey
64
- dtype: Optional override dtype when AugmentedValue is ambiguous, e.g. booleans
65
-
66
- Returns:
67
- DataKey: A rich DataKey describing the DB record
68
- """
69
- source = f"ca://{value.name}"
70
- assert value.ok, f"Error reading {source}: {value}"
71
-
72
- scalar = value.element_count == 1
73
- dtype = dtype or dbr_to_dtype[value.datatype] # type: ignore
74
-
75
- dtype_numpy = np.dtype(dbr.DbrCodeToType[value.datatype].dtype).descr[0][1]
76
-
77
- d = DataKey(
78
- source=source,
79
- dtype=dtype if scalar else "array",
80
- # Ignore until https://github.com/bluesky/event-model/issues/308
81
- dtype_numpy=dtype_numpy, # type: ignore
82
- # strictly value.element_count >= len(value)
83
- shape=[] if scalar else [len(value)],
84
- )
85
- for key in common_meta:
86
- attr = getattr(value, key, nan)
87
- if isinstance(attr, str) or not isnan(attr):
88
- d[key] = attr
89
-
90
- if choices is not None:
91
- d["choices"] = choices # type: ignore
92
-
93
- if limits := _limits_from_augmented_value(value):
94
- d["limits"] = limits # type: ignore
95
-
96
- return d
37
+ from ._common import format_datatype, get_supported_values
97
38
 
98
39
 
99
40
  def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
100
- def get_limits(limit: str) -> LimitPair:
41
+ def get_limits(limit: str) -> LimitsRange | None:
101
42
  low = getattr(value, f"lower_{limit}_limit", nan)
102
43
  high = getattr(value, f"upper_{limit}_limit", nan)
103
- return LimitPair(
104
- low=None if isnan(low) else low, high=None if isnan(high) else high
105
- )
106
-
107
- return Limits(
108
- alarm=get_limits("alarm"),
109
- control=get_limits("ctrl"),
110
- display=get_limits("disp"),
111
- warning=get_limits("warning"),
112
- )
113
-
44
+ if not (isnan(low) and isnan(high)):
45
+ return LimitsRange(
46
+ low=None if isnan(low) else low,
47
+ high=None if isnan(high) else high,
48
+ )
114
49
 
115
- @dataclass
116
- class CaConverter:
117
- read_dbr: Dbr | None
118
- write_dbr: Dbr | None
50
+ limits = Limits()
51
+ if limits_range := get_limits("alarm"):
52
+ limits["alarm"] = limits_range
53
+ if limits_range := get_limits("ctrl"):
54
+ limits["control"] = limits_range
55
+ if limits_range := get_limits("disp"):
56
+ limits["display"] = limits_range
57
+ if limits_range := get_limits("warning"):
58
+ limits["warning"] = limits_range
59
+ return limits
60
+
61
+
62
+ def _metadata_from_augmented_value(
63
+ value: AugmentedValue, metadata: SignalMetadata
64
+ ) -> SignalMetadata:
65
+ metadata = metadata.copy()
66
+ if hasattr(value, "units"):
67
+ metadata["units"] = value.units
68
+ if hasattr(value, "precision") and not isnan(value.precision):
69
+ metadata["precision"] = value.precision
70
+ if limits := _limits_from_augmented_value(value):
71
+ metadata["limits"] = limits
72
+ return metadata
73
+
74
+
75
+ class CaConverter(Generic[SignalDatatypeT]):
76
+ def __init__(
77
+ self,
78
+ datatype: type[SignalDatatypeT],
79
+ read_dbr: Dbr,
80
+ write_dbr: Dbr | None = None,
81
+ metadata: SignalMetadata | None = None,
82
+ ):
83
+ self.datatype = datatype
84
+ self.read_dbr: Dbr = read_dbr
85
+ self.write_dbr: Dbr | None = write_dbr
86
+ self.metadata = metadata or SignalMetadata()
119
87
 
120
- def write_value(self, value) -> Any:
88
+ def write_value(self, value: Any) -> Any:
89
+ # The ca library will do the conversion for us
121
90
  return value
122
91
 
123
- def value(self, value: AugmentedValue):
92
+ def value(self, value: AugmentedValue) -> SignalDatatypeT:
124
93
  # for channel access ca_xxx classes, this
125
94
  # invokes __pos__ operator to return an instance of
126
95
  # the builtin base class
127
96
  return +value # type: ignore
128
97
 
129
- def reading(self, value: AugmentedValue) -> Reading:
130
- return {
131
- "value": self.value(value),
132
- "timestamp": value.timestamp,
133
- "alarm_severity": -1 if value.severity > 2 else value.severity,
134
- }
135
-
136
- def get_datakey(self, value: AugmentedValue) -> DataKey:
137
- return _data_key_from_augmented_value(value)
138
98
 
139
-
140
- class CaLongStrConverter(CaConverter):
141
- def __init__(self):
142
- return super().__init__(dbr.DBR_CHAR_STR, dbr.DBR_CHAR_STR)
143
-
144
- def write_value(self, value: str):
145
- # Add a null in here as this is what the commandline caput does
146
- # TODO: this should be in the server so check if it can be pushed to asyn
147
- return value + "\0"
99
+ class DisconnectedCaConverter(CaConverter):
100
+ def __getattribute__(self, __name: str) -> Any:
101
+ raise NotImplementedError("No PV has been set as connect() has not been called")
148
102
 
149
103
 
150
- class CaArrayConverter(CaConverter):
151
- def value(self, value: AugmentedValue):
104
+ class CaArrayConverter(CaConverter[np.ndarray]):
105
+ def value(self, value: AugmentedValue) -> np.ndarray:
106
+ # A less expensive conversion
152
107
  return np.array(value, copy=False)
153
108
 
154
109
 
155
- @dataclass
156
- class CaEnumConverter(CaConverter):
157
- """To prevent issues when a signal is restarted and returns with different enum
158
- values or orders, we put treat an Enum signal as a string, and cache the
159
- choices on this class.
160
- """
110
+ class CaSequenceStrConverter(CaConverter[Sequence[str]]):
111
+ def value(self, value: AugmentedValue) -> Sequence[str]:
112
+ return [str(v) for v in value] # type: ignore
161
113
 
162
- choices: dict[str, str]
163
114
 
164
- def write_value(self, value: Enum | str):
165
- if isinstance(value, Enum):
166
- return value.value
167
- else:
168
- return value
115
+ class CaLongStrConverter(CaConverter[str]):
116
+ def __init__(self):
117
+ super().__init__(str, dbr.DBR_CHAR_STR, dbr.DBR_CHAR_STR)
169
118
 
170
- def value(self, value: AugmentedValue):
171
- return self.choices[value] # type: ignore
119
+ def write_value_and_dbr(self, value: Any) -> Any:
120
+ # Add a null in here as this is what the commandline caput does
121
+ # TODO: this should be in the server so check if it can be pushed to asyn
122
+ return value + "\0"
172
123
 
173
- def get_datakey(self, value: AugmentedValue) -> DataKey:
174
- # Sometimes DBR_TYPE returns as String, must pass choices still
175
- return _data_key_from_augmented_value(value, choices=list(self.choices.keys()))
176
124
 
125
+ class CaBoolConverter(CaConverter[bool]):
126
+ def __init__(self):
127
+ super().__init__(bool, dbr.DBR_SHORT)
177
128
 
178
- @dataclass
179
- class CaBoolConverter(CaConverter):
180
129
  def value(self, value: AugmentedValue) -> bool:
181
130
  return bool(value)
182
131
 
183
- def get_datakey(self, value: AugmentedValue) -> DataKey:
184
- return _data_key_from_augmented_value(value, dtype="boolean")
185
132
 
133
+ class CaEnumConverter(CaConverter[str]):
134
+ def __init__(self, supported_values: dict[str, str]):
135
+ self.supported_values = supported_values
136
+ super().__init__(
137
+ str, dbr.DBR_STRING, metadata=SignalMetadata(choices=list(supported_values))
138
+ )
186
139
 
187
- class DisconnectedCaConverter(CaConverter):
188
- def __getattribute__(self, __name: str) -> Any:
189
- raise NotImplementedError("No PV has been set as connect() has not been called")
140
+ def value(self, value: AugmentedValue) -> str:
141
+ return self.supported_values[str(value)]
142
+
143
+
144
+ _datatype_converter_from_dbr: dict[
145
+ tuple[Dbr, bool], tuple[type[SignalDatatype], type[CaConverter]]
146
+ ] = {
147
+ (dbr.DBR_STRING, False): (str, CaConverter),
148
+ (dbr.DBR_SHORT, False): (int, CaConverter),
149
+ (dbr.DBR_FLOAT, False): (float, CaConverter),
150
+ (dbr.DBR_ENUM, False): (str, CaConverter),
151
+ (dbr.DBR_CHAR, False): (int, CaConverter),
152
+ (dbr.DBR_LONG, False): (int, CaConverter),
153
+ (dbr.DBR_DOUBLE, False): (float, CaConverter),
154
+ (dbr.DBR_STRING, True): (Sequence[str], CaSequenceStrConverter),
155
+ (dbr.DBR_SHORT, True): (Array1D[np.int16], CaArrayConverter),
156
+ (dbr.DBR_FLOAT, True): (Array1D[np.float32], CaArrayConverter),
157
+ (dbr.DBR_ENUM, True): (Sequence[str], CaSequenceStrConverter),
158
+ (dbr.DBR_CHAR, True): (Array1D[np.uint8], CaArrayConverter),
159
+ (dbr.DBR_LONG, True): (Array1D[np.int32], CaArrayConverter),
160
+ (dbr.DBR_DOUBLE, True): (Array1D[np.float64], CaArrayConverter),
161
+ }
190
162
 
191
163
 
192
164
  def make_converter(
193
165
  datatype: type | None, values: dict[str, AugmentedValue]
194
166
  ) -> CaConverter:
195
167
  pv = list(values)[0]
196
- pv_dbr = get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
168
+ pv_dbr = cast(
169
+ Dbr, get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
170
+ )
197
171
  is_array = bool([v for v in values.values() if v.element_count > 1])
198
- if is_array and datatype is str and pv_dbr == dbr.DBR_CHAR:
172
+ # Infer a datatype and converter from the dbr
173
+ inferred_datatype, converter_cls = _datatype_converter_from_dbr[(pv_dbr, is_array)]
174
+ # Some override cases
175
+ if is_array and pv_dbr == dbr.DBR_CHAR and datatype is str:
199
176
  # Override waveform of chars to be treated as string
200
177
  return CaLongStrConverter()
201
- elif is_array and pv_dbr == dbr.DBR_STRING:
202
- # Waveform of strings, check we wanted this
203
- if datatype:
204
- datatype_dtype = get_dtype(datatype)
205
- if not datatype_dtype or not np.can_cast(datatype_dtype, np.str_):
206
- raise TypeError(f"{pv} has type [str] not {datatype.__name__}")
207
- return CaArrayConverter(pv_dbr, None)
208
- elif is_array:
209
- pv_dtype = get_unique({k: v.dtype for k, v in values.items()}, "dtypes") # type: ignore
210
- # This is an array
211
- if datatype:
212
- # Check we wanted an array of this type
213
- dtype = get_dtype(datatype)
214
- if not dtype:
215
- raise TypeError(f"{pv} has type [{pv_dtype}] not {datatype.__name__}")
216
- if dtype != pv_dtype:
217
- raise TypeError(f"{pv} has type [{pv_dtype}] not [{dtype}]")
218
- return CaArrayConverter(pv_dbr, None) # type: ignore
219
- elif pv_dbr == dbr.DBR_ENUM and datatype is bool:
220
- # Database can't do bools, so are often representated as enums,
221
- # CA can do int
222
- pv_choices_len = get_unique(
178
+ elif not is_array and datatype is bool and pv_dbr == dbr.DBR_ENUM:
179
+ # Database can't do bools, so are often representated as enums of len 2
180
+ pv_num_choices = get_unique(
223
181
  {k: len(v.enums) for k, v in values.items()}, "number of choices"
224
182
  )
225
- if pv_choices_len != 2:
226
- raise TypeError(f"{pv} has {pv_choices_len} choices, can't map to bool")
227
- return CaBoolConverter(dbr.DBR_SHORT, dbr.DBR_SHORT)
228
- elif pv_dbr == dbr.DBR_ENUM:
229
- # This is an Enum
183
+ if pv_num_choices != 2:
184
+ raise TypeError(f"{pv} has {pv_num_choices} choices, can't map to bool")
185
+ return CaBoolConverter()
186
+ elif not is_array and pv_dbr == dbr.DBR_ENUM:
230
187
  pv_choices = get_unique(
231
188
  {k: tuple(v.enums) for k, v in values.items()}, "choices"
232
189
  )
233
- supported_values = get_supported_values(pv, datatype, pv_choices)
234
- return CaEnumConverter(dbr.DBR_STRING, None, supported_values)
235
- else:
236
- value = list(values.values())[0]
237
- # Done the dbr check, so enough to check one of the values
238
- if datatype and not isinstance(value, datatype):
239
- # Allow int signals to represent float records when prec is 0
240
- is_prec_zero_float = (
241
- isinstance(value, float)
242
- and get_unique({k: v.precision for k, v in values.items()}, "precision")
243
- == 0
190
+ if enum_cls := get_enum_cls(datatype):
191
+ # If explicitly requested then check
192
+ return CaEnumConverter(get_supported_values(pv, enum_cls, pv_choices))
193
+ elif datatype in (None, str):
194
+ # Drop to string for safety, but retain choices as metadata
195
+ return CaConverter(
196
+ str,
197
+ dbr.DBR_STRING,
198
+ metadata=SignalMetadata(choices=list(pv_choices)),
244
199
  )
245
- if not (datatype is int and is_prec_zero_float):
246
- raise TypeError(
247
- f"{pv} has type {type(value).__name__.replace('ca_', '')} "
248
- + f"not {datatype.__name__}"
249
- )
250
- return CaConverter(pv_dbr, None) # type: ignore
200
+ elif (
201
+ inferred_datatype is float
202
+ and datatype is int
203
+ and get_unique({k: v.precision for k, v in values.items()}, "precision") == 0
204
+ ):
205
+ # Allow int signals to represent float records when prec is 0
206
+ return CaConverter(int, pv_dbr)
207
+ elif datatype in (None, inferred_datatype):
208
+ # If datatype matches what we are given then allow it and use inferred converter
209
+ return converter_cls(inferred_datatype, pv_dbr)
210
+ if pv_dbr == dbr.DBR_ENUM:
211
+ inferred_datatype = "str | SubsetEnum | StrictEnum"
212
+ raise TypeError(
213
+ f"{pv} with inferred datatype {format_datatype(inferred_datatype)}"
214
+ f" cannot be coerced to {format_datatype(datatype)}"
215
+ )
251
216
 
252
217
 
253
218
  _tried_pyepics = False
@@ -262,42 +227,24 @@ def _use_pyepics_context_if_imported():
262
227
  _tried_pyepics = True
263
228
 
264
229
 
265
- class CaSignalBackend(SignalBackend[T]):
266
- _ALLOWED_DATATYPES = (
267
- bool,
268
- int,
269
- float,
270
- str,
271
- Sequence,
272
- Enum,
273
- RuntimeSubsetEnum,
274
- np.ndarray,
275
- )
276
-
277
- @classmethod
278
- def datatype_allowed(cls, dtype: Any) -> bool:
279
- stripped_origin = get_origin(dtype) or dtype
280
- if dtype is None:
281
- return True
282
-
283
- return inspect.isclass(stripped_origin) and issubclass(
284
- stripped_origin, cls._ALLOWED_DATATYPES
285
- )
286
-
287
- def __init__(self, datatype: type[T] | None, read_pv: str, write_pv: str):
288
- self.datatype = datatype
289
- if not CaSignalBackend.datatype_allowed(self.datatype):
290
- raise TypeError(f"Given datatype {self.datatype} unsupported in CA.")
230
+ class CaSignalBackend(SignalBackend[SignalDatatypeT]):
231
+ def __init__(
232
+ self,
233
+ datatype: type[SignalDatatypeT] | None,
234
+ read_pv: str = "",
235
+ write_pv: str = "",
236
+ ):
291
237
  self.read_pv = read_pv
292
238
  self.write_pv = write_pv
239
+ self.converter: CaConverter = DisconnectedCaConverter(float, dbr.DBR_DOUBLE)
293
240
  self.initial_values: dict[str, AugmentedValue] = {}
294
- self.converter: CaConverter = DisconnectedCaConverter(None, None)
295
241
  self.subscription: Subscription | None = None
242
+ super().__init__(datatype)
296
243
 
297
- def source(self, name: str):
298
- return f"ca://{self.read_pv}"
244
+ def source(self, name: str, read: bool):
245
+ return f"ca://{self.read_pv if read else self.write_pv}"
299
246
 
300
- async def _store_initial_value(self, pv, timeout: float = DEFAULT_TIMEOUT):
247
+ async def _store_initial_value(self, pv: str, timeout: float):
301
248
  try:
302
249
  self.initial_values[pv] = await caget(
303
250
  pv, format=FORMAT_CTRL, timeout=timeout
@@ -306,7 +253,7 @@ class CaSignalBackend(SignalBackend[T]):
306
253
  logging.debug(f"signal ca://{pv} timed out")
307
254
  raise NotConnected(f"ca://{pv}") from exc
308
255
 
309
- async def connect(self, timeout: float = DEFAULT_TIMEOUT):
256
+ async def connect(self, timeout: float):
310
257
  _use_pyepics_context_if_imported()
311
258
  if self.read_pv != self.write_pv:
312
259
  # Different, need to connect both
@@ -319,7 +266,19 @@ class CaSignalBackend(SignalBackend[T]):
319
266
  await self._store_initial_value(self.read_pv, timeout=timeout)
320
267
  self.converter = make_converter(self.datatype, self.initial_values)
321
268
 
322
- async def put(self, value: T | None, wait=True, timeout=None):
269
+ async def _caget(self, pv: str, format: Format) -> AugmentedValue:
270
+ return await caget(
271
+ pv, datatype=self.converter.read_dbr, format=format, timeout=None
272
+ )
273
+
274
+ def _make_reading(self, value: AugmentedValue) -> Reading[SignalDatatypeT]:
275
+ return {
276
+ "value": self.converter.value(value),
277
+ "timestamp": value.timestamp,
278
+ "alarm_severity": -1 if value.severity > 2 else value.severity,
279
+ }
280
+
281
+ async def put(self, value: SignalDatatypeT | None, wait: bool):
323
282
  if value is None:
324
283
  write_value = self.initial_values[self.write_pv]
325
284
  else:
@@ -329,50 +288,39 @@ class CaSignalBackend(SignalBackend[T]):
329
288
  write_value,
330
289
  datatype=self.converter.write_dbr,
331
290
  wait=wait,
332
- timeout=timeout,
333
- )
334
-
335
- async def _caget(self, format: Format) -> AugmentedValue:
336
- return await caget(
337
- self.read_pv,
338
- datatype=self.converter.read_dbr,
339
- format=format,
340
291
  timeout=None,
341
292
  )
342
293
 
343
294
  async def get_datakey(self, source: str) -> DataKey:
344
- value = await self._caget(FORMAT_CTRL)
345
- return self.converter.get_datakey(value)
295
+ value = await self._caget(self.read_pv, FORMAT_CTRL)
296
+ metadata = _metadata_from_augmented_value(value, self.converter.metadata)
297
+ return make_datakey(
298
+ self.converter.datatype, self.converter.value(value), source, metadata
299
+ )
346
300
 
347
- async def get_reading(self) -> Reading:
348
- value = await self._caget(FORMAT_TIME)
349
- return self.converter.reading(value)
301
+ async def get_reading(self) -> Reading[SignalDatatypeT]:
302
+ value = await self._caget(self.read_pv, FORMAT_TIME)
303
+ return self._make_reading(value)
350
304
 
351
- async def get_value(self) -> T:
352
- value = await self._caget(FORMAT_RAW)
305
+ async def get_value(self) -> SignalDatatypeT:
306
+ value = await self._caget(self.read_pv, FORMAT_RAW)
353
307
  return self.converter.value(value)
354
308
 
355
- async def get_setpoint(self) -> T:
356
- value = await caget(
357
- self.write_pv,
358
- datatype=self.converter.read_dbr,
359
- format=FORMAT_RAW,
360
- timeout=None,
361
- )
309
+ async def get_setpoint(self) -> SignalDatatypeT:
310
+ value = await self._caget(self.write_pv, FORMAT_RAW)
362
311
  return self.converter.value(value)
363
312
 
364
- def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
313
+ def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
365
314
  if callback:
366
315
  assert (
367
316
  not self.subscription
368
317
  ), "Cannot set a callback when one is already set"
369
318
  self.subscription = camonitor(
370
319
  self.read_pv,
371
- lambda v: callback(self.converter.reading(v), self.converter.value(v)),
320
+ lambda v: callback(self._make_reading(v)),
372
321
  datatype=self.converter.read_dbr,
373
322
  format=FORMAT_TIME,
374
323
  )
375
- else:
376
- if self.subscription:
377
- self.subscription.close()
324
+ elif self.subscription:
325
+ self.subscription.close()
378
326
  self.subscription = None
@@ -1,57 +1,43 @@
1
- import inspect
2
- from enum import Enum
1
+ from collections.abc import Sequence
2
+ from typing import Any, get_args, get_origin
3
3
 
4
- from typing_extensions import TypedDict
4
+ import numpy as np
5
5
 
6
- from ophyd_async.core import RuntimeSubsetEnum
7
-
8
- common_meta = {
9
- "units",
10
- "precision",
11
- }
12
-
13
-
14
- class LimitPair(TypedDict):
15
- high: float | None
16
- low: float | None
17
-
18
-
19
- class Limits(TypedDict):
20
- alarm: LimitPair
21
- control: LimitPair
22
- display: LimitPair
23
- warning: LimitPair
6
+ from ophyd_async.core import SubsetEnum, get_dtype, get_enum_cls
24
7
 
25
8
 
26
9
  def get_supported_values(
27
10
  pv: str,
28
- datatype: type[str] | None,
29
- pv_choices: tuple[str, ...],
11
+ datatype: type,
12
+ pv_choices: Sequence[str],
30
13
  ) -> dict[str, str]:
31
- if inspect.isclass(datatype) and issubclass(datatype, RuntimeSubsetEnum):
32
- if not set(datatype.choices).issubset(set(pv_choices)):
33
- raise TypeError(
34
- f"{pv} has choices {pv_choices}, "
35
- f"which is not a superset of {str(datatype)}."
36
- )
37
- return {x: x or "_" for x in pv_choices}
38
- elif inspect.isclass(datatype) and issubclass(datatype, Enum):
39
- if not issubclass(datatype, str):
40
- raise TypeError(
41
- f"{pv} is type Enum but {datatype} does not inherit from String."
42
- )
43
-
44
- choices = tuple(v.value for v in datatype)
14
+ enum_cls = get_enum_cls(datatype)
15
+ if not enum_cls:
16
+ raise TypeError(f"{datatype} is not an Enum")
17
+ choices = [v.value for v in enum_cls]
18
+ error_msg = f"{pv} has choices {pv_choices}, but {datatype} requested {choices} "
19
+ if issubclass(enum_cls, SubsetEnum):
20
+ if not set(choices).issubset(pv_choices):
21
+ raise TypeError(error_msg + "to be a subset of them.")
22
+ else:
45
23
  if set(choices) != set(pv_choices):
46
- raise TypeError(
47
- f"{pv} has choices {pv_choices}, "
48
- f"which do not match {datatype}, which has {choices}."
49
- )
50
- return {x: datatype(x) if x else "_" for x in pv_choices}
51
- elif datatype is None or datatype is str:
52
- return {x: x or "_" for x in pv_choices}
53
-
54
- raise TypeError(
55
- f"{pv} has choices {pv_choices}. "
56
- "Use an Enum or SubsetEnum to represent this."
57
- )
24
+ raise TypeError(error_msg + "to be strictly equal to them.")
25
+
26
+ # Take order from the pv choices
27
+ supported_values = {x: x for x in pv_choices}
28
+ # But override those that we specify via the datatype
29
+ for v in enum_cls:
30
+ supported_values[v.value] = v
31
+ return supported_values
32
+
33
+
34
+ def format_datatype(datatype: Any) -> str:
35
+ if get_origin(datatype) is np.ndarray and get_args(datatype)[0] == tuple[int]:
36
+ dtype = get_dtype(datatype)
37
+ return f"Array1D[np.{dtype.name}]"
38
+ elif get_origin(datatype) is Sequence:
39
+ return f"Sequence[{get_args(datatype)[0].__name__}]"
40
+ elif isinstance(datatype, type):
41
+ return datatype.__name__
42
+ else:
43
+ return str(datatype)