ophyd-async 0.13.3__py3-none-any.whl → 0.13.5__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 (42) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +26 -3
  3. ophyd_async/core/_derived_signal_backend.py +2 -1
  4. ophyd_async/core/_detector.py +2 -2
  5. ophyd_async/core/_device.py +9 -9
  6. ophyd_async/core/_enums.py +5 -0
  7. ophyd_async/core/_signal.py +34 -38
  8. ophyd_async/core/_signal_backend.py +3 -1
  9. ophyd_async/core/_status.py +2 -2
  10. ophyd_async/core/_table.py +8 -0
  11. ophyd_async/core/_utils.py +11 -11
  12. ophyd_async/epics/adcore/_core_logic.py +3 -1
  13. ophyd_async/epics/adcore/_utils.py +4 -4
  14. ophyd_async/epics/core/_aioca.py +2 -2
  15. ophyd_async/epics/core/_p4p.py +2 -2
  16. ophyd_async/epics/motor.py +28 -7
  17. ophyd_async/epics/pmac/_pmac_io.py +8 -4
  18. ophyd_async/epics/pmac/_pmac_trajectory.py +144 -41
  19. ophyd_async/epics/pmac/_pmac_trajectory_generation.py +692 -0
  20. ophyd_async/epics/pmac/_utils.py +1 -681
  21. ophyd_async/fastcs/jungfrau/__init__.py +2 -1
  22. ophyd_async/fastcs/jungfrau/_controller.py +29 -11
  23. ophyd_async/fastcs/jungfrau/_utils.py +10 -2
  24. ophyd_async/fastcs/panda/__init__.py +10 -0
  25. ophyd_async/fastcs/panda/_block.py +14 -0
  26. ophyd_async/fastcs/panda/_trigger.py +123 -3
  27. ophyd_async/sim/_motor.py +4 -2
  28. ophyd_async/sim/_stage.py +14 -4
  29. ophyd_async/tango/core/__init__.py +17 -3
  30. ophyd_async/tango/core/_signal.py +18 -22
  31. ophyd_async/tango/core/_tango_transport.py +407 -239
  32. ophyd_async/tango/core/_utils.py +9 -0
  33. ophyd_async/tango/demo/_mover.py +1 -2
  34. ophyd_async/tango/testing/__init__.py +2 -1
  35. ophyd_async/tango/testing/_one_of_everything.py +13 -5
  36. ophyd_async/tango/testing/_test_config.py +11 -0
  37. ophyd_async/testing/_assert.py +2 -2
  38. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/METADATA +2 -36
  39. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/RECORD +42 -40
  40. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/WHEEL +0 -0
  41. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/licenses/LICENSE +0 -0
  42. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.13.3'
32
- __version_tuple__ = version_tuple = (0, 13, 3)
31
+ __version__ = version = '0.13.5'
32
+ __version_tuple__ = version_tuple = (0, 13, 5)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -21,6 +21,7 @@ from ._enums import (
21
21
  EnableDisable,
22
22
  InOut,
23
23
  OnOff,
24
+ YesNo,
24
25
  )
25
26
  from ._flyer import FlyerController, FlyMotorInfo, StandardFlyer
26
27
  from ._hdf_dataset import HDFDatasetDescription, HDFDocumentComposer
@@ -78,7 +79,7 @@ from ._signal_backend import (
78
79
  )
79
80
  from ._soft_signal_backend import SoftSignalBackend
80
81
  from ._status import AsyncStatus, WatchableAsyncStatus, completed_status
81
- from ._table import Table
82
+ from ._table import Table, TableSubclass
82
83
  from ._utils import (
83
84
  CALCULATE_TIMEOUT,
84
85
  DEFAULT_TIMEOUT,
@@ -87,7 +88,7 @@ from ._utils import (
87
88
  ConfinedModel,
88
89
  EnumTypes,
89
90
  LazyMock,
90
- NotConnected,
91
+ NotConnectedError,
91
92
  Reference,
92
93
  StrictEnum,
93
94
  SubsetEnum,
@@ -103,6 +104,26 @@ from ._utils import (
103
104
  )
104
105
  from ._yaml_settings import YamlSettingsProvider
105
106
 
107
+
108
+ # Back compat - delete before 1.0
109
+ def __getattr__(name):
110
+ import warnings
111
+
112
+ renames = {
113
+ "NotConnected": NotConnectedError,
114
+ }
115
+ rename = renames.get(name)
116
+ if rename is not None:
117
+ warnings.warn(
118
+ DeprecationWarning(
119
+ f"{name!r} is deprecated, use {rename.__name__!r} instead"
120
+ ),
121
+ stacklevel=2,
122
+ )
123
+ return rename
124
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
125
+
126
+
106
127
  __all__ = [
107
128
  # Device
108
129
  "Device",
@@ -195,7 +216,7 @@ __all__ = [
195
216
  "DEFAULT_TIMEOUT",
196
217
  "Callback",
197
218
  "ConfinedModel",
198
- "NotConnected",
219
+ "NotConnectedError",
199
220
  "Reference",
200
221
  "error_if_none",
201
222
  "gather_dict",
@@ -221,4 +242,6 @@ __all__ = [
221
242
  "EnableDisable",
222
243
  "InOut",
223
244
  "OnOff",
245
+ "YesNo",
246
+ "TableSubclass",
224
247
  ]
@@ -222,7 +222,8 @@ class SignalTransformer(Generic[TransformT]):
222
222
  # callback
223
223
  self._cached_readings = {}
224
224
  for raw in self.raw_and_transform_subscribables.values():
225
- raw.subscribe(self._update_cached_reading)
225
+ # can remove type: ignore when Subscribable protocol updated
226
+ raw.subscribe_reading(self._update_cached_reading) # type: ignore
226
227
  elif self._complete_cached_reading():
227
228
  # Callback on the last complete set of readings
228
229
  derived_readings = self._make_derived_readings(self._cached_readings)
@@ -249,11 +249,11 @@ class StandardDetector(
249
249
  )
250
250
  try:
251
251
  await signal.get_value()
252
- except NotImplementedError as e:
252
+ except NotImplementedError as exc:
253
253
  raise Exception(
254
254
  f"config signal {signal.name} must be connected before it is "
255
255
  + "passed to the detector"
256
- ) from e
256
+ ) from exc
257
257
 
258
258
  @AsyncStatus.wrap
259
259
  async def unstage(self) -> None:
@@ -13,7 +13,7 @@ from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
13
13
  from ._utils import (
14
14
  DEFAULT_TIMEOUT,
15
15
  LazyMock,
16
- NotConnected,
16
+ NotConnectedError,
17
17
  error_if_none,
18
18
  wait_for_connection,
19
19
  )
@@ -51,10 +51,10 @@ class DeviceConnector:
51
51
  for name, child_device in device.children():
52
52
  try:
53
53
  await child_device.connect(mock=mock.child(name))
54
- except Exception as e:
55
- exceptions[name] = e
54
+ except Exception as exc:
55
+ exceptions[name] = exc
56
56
  if exceptions:
57
- raise NotConnected.with_other_exceptions_logged(exceptions)
57
+ raise NotConnectedError.with_other_exceptions_logged(exceptions)
58
58
 
59
59
  async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
60
60
  """Use during [](#Device.connect) with `mock=False`.
@@ -62,7 +62,7 @@ class DeviceConnector:
62
62
  This is called when there is no cached connect done in `mock=False`
63
63
  mode. It connects the Device and all its children in real mode in parallel.
64
64
  """
65
- # Connect in parallel, gathering up NotConnected errors
65
+ # Connect in parallel, gathering up NotConnectedErrors
66
66
  coros = {
67
67
  name: child_device.connect(timeout=timeout, force_reconnect=force_reconnect)
68
68
  for name, child_device in device.children()
@@ -334,12 +334,12 @@ class DeviceProcessor:
334
334
  self._locals_on_exit = self._caller_locals()
335
335
  try:
336
336
  fut = call_in_bluesky_event_loop(self._on_exit())
337
- except RuntimeError as e:
338
- raise NotConnected(
337
+ except RuntimeError as exc:
338
+ raise NotConnectedError(
339
339
  "Could not connect devices. Is the bluesky event loop running? See "
340
340
  "https://blueskyproject.io/ophyd-async/main/"
341
341
  "user/explanations/event-loop-choice.html for more info."
342
- ) from e
342
+ ) from exc
343
343
  return fut
344
344
 
345
345
  async def _on_exit(self) -> None:
@@ -372,7 +372,7 @@ def init_devices(
372
372
  :param mock: If True, connect Signals in mock mode.
373
373
  :param timeout: How long to wait for connect before logging an exception.
374
374
  :raises RuntimeError: If used inside a plan, use [](#ensure_connected) instead.
375
- :raises NotConnected: If devices could not be connected.
375
+ :raises NotConnectedError: If devices could not be connected.
376
376
 
377
377
  For example, to connect and name 2 motors in parallel:
378
378
  ```python
@@ -19,3 +19,8 @@ class EnabledDisabled(StrictEnum):
19
19
  class InOut(StrictEnum):
20
20
  IN = "In"
21
21
  OUT = "Out"
22
+
23
+
24
+ class YesNo(StrictEnum):
25
+ YES = "Yes"
26
+ NO = "No"
@@ -39,8 +39,8 @@ from ._utils import (
39
39
  async def _wait_for(coro: Awaitable[T], timeout: float | None, source: str) -> T:
40
40
  try:
41
41
  return await asyncio.wait_for(coro, timeout)
42
- except TimeoutError as e:
43
- raise TimeoutError(source) from e
42
+ except TimeoutError as exc:
43
+ raise TimeoutError(source) from exc
44
44
 
45
45
 
46
46
  def _add_timeout(func):
@@ -117,17 +117,17 @@ class _SignalCache(Generic[SignalDatatypeT]):
117
117
  def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal) -> None:
118
118
  self._signal: Signal[Any] = signal
119
119
  self._staged = False
120
- self._listeners: dict[Callback, bool] = {}
120
+ self._listeners: set[Callback] = set()
121
121
  self._valid = asyncio.Event()
122
122
  self._reading: Reading[SignalDatatypeT] | None = None
123
123
  self.backend: SignalBackend[SignalDatatypeT] = backend
124
124
  try:
125
125
  asyncio.get_running_loop()
126
- except RuntimeError as e:
126
+ except RuntimeError as exc:
127
127
  raise RuntimeError(
128
128
  "Need a running event loop to subscribe to a signal, "
129
129
  "are you trying to run subscribe outside a plan?"
130
- ) from e
130
+ ) from exc
131
131
  signal.log.debug(f"Making subscription on source {signal.source}")
132
132
  backend.set_callback(self._callback)
133
133
 
@@ -154,29 +154,29 @@ class _SignalCache(Generic[SignalDatatypeT]):
154
154
  )
155
155
  self._reading = reading
156
156
  self._valid.set()
157
- items = self._listeners.copy().items()
158
- for function, want_value in items:
159
- self._notify(function, want_value)
157
+ # Copy the listeners in case one of the callbacks removes the listener
158
+ # from the set
159
+ for callback in list(self._listeners):
160
+ self._notify(callback)
160
161
 
161
162
  def _notify(
162
163
  self,
163
- function: Callback[dict[str, Reading[SignalDatatypeT]] | SignalDatatypeT],
164
- want_value: bool,
164
+ function: Callback[dict[str, Reading[SignalDatatypeT]]],
165
165
  ) -> None:
166
- function(self._ensure_reading()["value"]) if want_value else function(
167
- {self._signal.name: self._ensure_reading()}
168
- )
166
+ function({self._signal.name: self._ensure_reading()})
169
167
 
170
- def subscribe(self, function: Callback, want_value: bool) -> None:
171
- self._listeners[function] = want_value
168
+ def subscribe(self, function: Callback) -> None:
169
+ self._listeners.add(function)
172
170
  if self._valid.is_set():
173
- self._notify(function, want_value)
171
+ self._notify(function)
174
172
 
175
173
  def unsubscribe(self, function: Callback) -> bool:
176
- _listener = self._listeners.pop(function, None)
177
- if not _listener:
174
+ if function in self._listeners:
175
+ self._listeners.remove(function)
176
+ else:
178
177
  self._signal.log.warning(
179
- f"Unsubscribe failed: subscriber {function} was not found "
178
+ f"Unsubscribe failed for signal {self._signal.name}:"
179
+ f" subscriber {function} was not found"
180
180
  f" in listeners list: {list(self._listeners)}"
181
181
  )
182
182
  return self._staged or bool(self._listeners)
@@ -244,24 +244,19 @@ class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribab
244
244
  self.log.debug(f"get_value() on source {self.source} returned {value}")
245
245
  return value
246
246
 
247
- def subscribe_value(self, function: Callback[SignalDatatypeT]):
248
- """Subscribe to updates in value of a device.
249
-
250
- :param function: The callback function to call when the value changes.
251
- """
252
- self._get_cache().subscribe(function, want_value=True)
253
-
254
- def subscribe(
247
+ def subscribe_reading(
255
248
  self, function: Callback[dict[str, Reading[SignalDatatypeT]]]
256
249
  ) -> None:
257
250
  """Subscribe to updates in the reading.
258
251
 
259
252
  :param function: The callback function to call when the reading changes.
260
253
  """
261
- self._get_cache().subscribe(function, want_value=False)
254
+ self._get_cache().subscribe(function)
255
+
256
+ subscribe = subscribe_reading
262
257
 
263
258
  def clear_sub(self, function: Callback) -> None:
264
- """Remove a subscription passed to `subscribe` or `subscribe_value`.
259
+ """Remove a subscription passed to `subscribe_reading`.
265
260
 
266
261
  :param function: The callback function to remove.
267
262
  """
@@ -402,7 +397,7 @@ async def observe_value(
402
397
  value being yielded, even if it is the same as the previous value.
403
398
 
404
399
  :param signal:
405
- Call subscribe_value on this at the start, and clear_sub on it at the end.
400
+ Call subscribe_reading on this at the start, and clear_sub on it at the end.
406
401
  :param timeout:
407
402
  If given, how long to wait for each updated value in seconds. If an
408
403
  update is not produced in this time then raise asyncio.TimeoutError.
@@ -454,7 +449,7 @@ async def observe_signals_value(
454
449
  value being yielded, even if it is the same as the previous value.
455
450
 
456
451
  :param signals:
457
- Call subscribe_value on all the signals at the start, and clear_sub on
452
+ Call subscribe_reading on all the signals at the start, and clear_sub on
458
453
  it at the end.
459
454
  :param timeout:
460
455
  If given, how long to wait for ANY updated value from shared queue in seconds.
@@ -486,11 +481,12 @@ async def observe_signals_value(
486
481
  # subscribe signal to update queue and fill cbs dict
487
482
  for signal in signals:
488
483
 
489
- def queue_value(value: SignalDatatypeT, signal=signal):
484
+ def queue_value(reading: dict[str, Reading[SignalDatatypeT]], signal=signal):
485
+ value = reading[signal.name]["value"]
490
486
  q.put_nowait((signal, value))
491
487
 
492
488
  cbs[signal] = queue_value
493
- signal.subscribe_value(queue_value)
489
+ signal.subscribe_reading(queue_value)
494
490
 
495
491
  if done_status is not None:
496
492
  done_status.add_callback(q.put_nowait)
@@ -543,11 +539,11 @@ class _ValueChecker(Generic[SignalDatatypeT]):
543
539
  ):
544
540
  try:
545
541
  await asyncio.wait_for(self._wait_for_value(signal), timeout)
546
- except TimeoutError as e:
542
+ except TimeoutError as exc:
547
543
  raise TimeoutError(
548
544
  f"{signal.name} didn't match {self._matcher_name} in {timeout}s, "
549
545
  f"last value {self._last_value!r}"
550
- ) from e
546
+ ) from exc
551
547
 
552
548
 
553
549
  async def wait_for_value(
@@ -558,7 +554,7 @@ async def wait_for_value(
558
554
  """Wait for a signal to have a matching value.
559
555
 
560
556
  :param signal:
561
- Call subscribe_value on this at the start, and clear_sub on it at the
557
+ Call subscribe_reading on this at the start, and clear_sub on it at the
562
558
  end.
563
559
  :param match:
564
560
  If a callable, it should return True if the value matches. If not
@@ -642,11 +638,11 @@ async def set_and_wait_for_other_value(
642
638
  await asyncio.wait_for(_wait_for_value(), timeout)
643
639
  if wait_for_set_completion:
644
640
  await status
645
- except TimeoutError as e:
641
+ except TimeoutError as exc:
646
642
  raise TimeoutError(
647
643
  f"{match_signal.name} value didn't match value from"
648
644
  f" {matcher.__name__}() in {timeout}s"
649
- ) from e
645
+ ) from exc
650
646
 
651
647
  return status
652
648
 
@@ -184,7 +184,9 @@ def _datakey_shape(value: SignalDatatype) -> list[int | None]:
184
184
  elif isinstance(value, Sequence | Table):
185
185
  return [len(value)]
186
186
  else:
187
- raise TypeError(f"Can't make shape for {value}")
187
+ raise TypeError(
188
+ f"Can't make shape for {value} with SignalDataType: {type(value)}"
189
+ )
188
190
 
189
191
 
190
192
  def make_datakey(
@@ -55,8 +55,8 @@ class AsyncStatusBase(Status, Awaitable[None]):
55
55
  if self.task.done():
56
56
  try:
57
57
  return self.task.exception()
58
- except asyncio.CancelledError as e:
59
- return e
58
+ except asyncio.CancelledError as exc:
59
+ return exc
60
60
  return None
61
61
 
62
62
  @property
@@ -13,6 +13,7 @@ TableSubclass = TypeVar("TableSubclass", bound="Table")
13
13
 
14
14
 
15
15
  def _concat(value1, value2):
16
+ """Concatenate two values, supports both NumPy arrays and generic types."""
16
17
  if isinstance(value1, np.ndarray):
17
18
  return np.concatenate((value1, value2))
18
19
  else:
@@ -20,6 +21,8 @@ def _concat(value1, value2):
20
21
 
21
22
 
22
23
  def _make_default_factory(dtype: np.dtype) -> Callable[[], np.ndarray]:
24
+ """Creates a default factory, returns an empty Numpy array of a specified dtype."""
25
+
23
26
  def numpy_array_default_factory() -> np.ndarray:
24
27
  return np.array([], dtype)
25
28
 
@@ -91,6 +94,11 @@ class Table(ConfinedModel):
91
94
  raise TypeError(f"Cannot use annotation {anno} in a Table")
92
95
  cls.__annotations__[k] = new_anno
93
96
 
97
+ @classmethod
98
+ def empty(cls: type[TableSubclass]) -> TableSubclass:
99
+ """Makes an empty table with zero length columns."""
100
+ return cls() # type: ignore
101
+
94
102
  def __add__(self, right: TableSubclass) -> TableSubclass:
95
103
  """Concatenate the arrays in field values."""
96
104
  cls = type(right)
@@ -38,7 +38,7 @@ class UppercaseNameEnumMeta(EnumMeta):
38
38
 
39
39
 
40
40
  class AnyStringUppercaseNameEnumMeta(UppercaseNameEnumMeta):
41
- def __call__(self, value, *args, **kwargs): # type: ignore
41
+ def __call__(cls, value, *args, **kwargs): # type: ignore
42
42
  """Return given value if it is a string and not a member of the enum.
43
43
 
44
44
  If the value is not a string or is an enum member, default enum behavior
@@ -54,7 +54,7 @@ class AnyStringUppercaseNameEnumMeta(UppercaseNameEnumMeta):
54
54
  member.
55
55
 
56
56
  """
57
- if isinstance(value, str) and not isinstance(value, self):
57
+ if isinstance(value, str) and not isinstance(value, cls):
58
58
  return value
59
59
  return super().__call__(value, *args, **kwargs)
60
60
 
@@ -85,11 +85,11 @@ timeout itself
85
85
  CalculatableTimeout = float | None | Literal["CALCULATE_TIMEOUT"]
86
86
 
87
87
 
88
- class NotConnected(Exception):
88
+ class NotConnectedError(Exception):
89
89
  """Exception to be raised if a `Device.connect` is cancelled.
90
90
 
91
91
  :param errors:
92
- Mapping of device name to Exception or another NotConnected.
92
+ Mapping of device name to Exception or another NotConnectedError.
93
93
  Alternatively a string with the signal error text.
94
94
  """
95
95
 
@@ -106,7 +106,7 @@ class NotConnected(Exception):
106
106
  return {}
107
107
 
108
108
  def _format_sub_errors(self, name: str, error: Exception, indent="") -> str:
109
- if isinstance(error, NotConnected):
109
+ if isinstance(error, NotConnectedError):
110
110
  error_txt = ":" + error.format_error_string(indent + self._indent_width)
111
111
  elif isinstance(error, Exception):
112
112
  error_txt = ": " + err_str + "\n" if (err_str := str(error)) else "\n"
@@ -138,15 +138,15 @@ class NotConnected(Exception):
138
138
  @classmethod
139
139
  def with_other_exceptions_logged(
140
140
  cls, exceptions: Mapping[str, Exception]
141
- ) -> NotConnected:
141
+ ) -> NotConnectedError:
142
142
  for name, exception in exceptions.items():
143
- if not isinstance(exception, NotConnected):
143
+ if not isinstance(exception, NotConnectedError):
144
144
  logger.exception(
145
145
  f"device `{name}` raised unexpected exception "
146
146
  f"{type(exception).__name__}",
147
147
  exc_info=exception,
148
148
  )
149
- return NotConnected(exceptions)
149
+ return NotConnectedError(exceptions)
150
150
 
151
151
 
152
152
  @dataclass(frozen=True)
@@ -192,8 +192,8 @@ async def wait_for_connection(**coros: Awaitable[None]):
192
192
  name, coro = coros.popitem()
193
193
  try:
194
194
  await coro
195
- except Exception as e:
196
- exceptions[name] = e
195
+ except Exception as exc:
196
+ exceptions[name] = exc
197
197
  else:
198
198
  # Use gather to connect in parallel
199
199
  results = await asyncio.gather(*coros.values(), return_exceptions=True)
@@ -202,7 +202,7 @@ async def wait_for_connection(**coros: Awaitable[None]):
202
202
  exceptions[name] = result
203
203
 
204
204
  if exceptions:
205
- raise NotConnected.with_other_exceptions_logged(exceptions)
205
+ raise NotConnectedError.with_other_exceptions_logged(exceptions)
206
206
 
207
207
 
208
208
  def get_dtype(datatype: type) -> np.dtype:
@@ -34,9 +34,11 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
34
34
  self,
35
35
  driver: ADBaseIOT,
36
36
  good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
37
+ image_mode: ADImageMode = ADImageMode.MULTIPLE,
37
38
  ) -> None:
38
39
  self.driver: ADBaseIOT = driver
39
40
  self.good_states = good_states
41
+ self.image_mode = image_mode
40
42
  self.frame_timeout = DEFAULT_TIMEOUT
41
43
  self._arm_status: AsyncStatus | None = None
42
44
 
@@ -52,7 +54,7 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
52
54
  )
53
55
  await asyncio.gather(
54
56
  self.driver.num_images.set(trigger_info.total_number_of_exposures),
55
- self.driver.image_mode.set(ADImageMode.MULTIPLE),
57
+ self.driver.image_mode.set(self.image_mode),
56
58
  )
57
59
 
58
60
  async def arm(self):
@@ -63,8 +63,8 @@ def convert_pv_dtype_to_np(datatype: str) -> str:
63
63
  else:
64
64
  try:
65
65
  np_datatype = convert_ad_dtype_to_np(_pvattribute_to_ad_datatype[datatype])
66
- except KeyError as e:
67
- raise ValueError(f"Invalid dbr type {datatype}") from e
66
+ except KeyError as exc:
67
+ raise ValueError(f"Invalid dbr type {datatype}") from exc
68
68
  return np_datatype
69
69
 
70
70
 
@@ -81,8 +81,8 @@ def convert_param_dtype_to_np(datatype: str) -> str:
81
81
  np_datatype = convert_ad_dtype_to_np(
82
82
  _paramattribute_to_ad_datatype[datatype]
83
83
  )
84
- except KeyError as e:
85
- raise ValueError(f"Invalid datatype {datatype}") from e
84
+ except KeyError as exc:
85
+ raise ValueError(f"Invalid datatype {datatype}") from exc
86
86
  return np_datatype
87
87
 
88
88
 
@@ -26,7 +26,7 @@ from event_model import DataKey, Limits, LimitsRange
26
26
  from ophyd_async.core import (
27
27
  Array1D,
28
28
  Callback,
29
- NotConnected,
29
+ NotConnectedError,
30
30
  SignalDatatype,
31
31
  SignalDatatypeT,
32
32
  SignalMetadata,
@@ -272,7 +272,7 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
272
272
  )
273
273
  except CANothing as exc:
274
274
  logger.debug(f"signal ca://{pv} timed out")
275
- raise NotConnected(f"ca://{pv}") from exc
275
+ raise NotConnectedError(f"ca://{pv}") from exc
276
276
 
277
277
  async def connect(self, timeout: float):
278
278
  _use_pyepics_context_if_imported()
@@ -18,7 +18,7 @@ from pydantic import BaseModel
18
18
  from ophyd_async.core import (
19
19
  Array1D,
20
20
  Callback,
21
- NotConnected,
21
+ NotConnectedError,
22
22
  SignalDatatype,
23
23
  SignalDatatypeT,
24
24
  SignalMetadata,
@@ -328,7 +328,7 @@ async def pvget_with_timeout(pv: str, timeout: float) -> Any:
328
328
  return await asyncio.wait_for(context().get(pv), timeout=timeout)
329
329
  except TimeoutError as exc:
330
330
  logger.debug(f"signal pva://{pv} timed out", exc_info=True)
331
- raise NotConnected(f"pva://{pv}") from exc
331
+ raise NotConnectedError(f"pva://{pv}") from exc
332
332
 
333
333
 
334
334
  def _pva_request_string(fields: Sequence[str]) -> str:
@@ -32,15 +32,34 @@ from ophyd_async.core import (
32
32
  from ophyd_async.core import StandardReadableFormat as Format
33
33
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
34
34
 
35
- __all__ = ["MotorLimitsException", "Motor"]
35
+ __all__ = ["MotorLimitsError", "Motor"]
36
36
 
37
37
 
38
- class MotorLimitsException(Exception):
38
+ class MotorLimitsError(Exception):
39
39
  """Exception for invalid motor limits."""
40
40
 
41
41
  pass
42
42
 
43
43
 
44
+ # Back compat - delete before 1.0
45
+ def __getattr__(name):
46
+ import warnings
47
+
48
+ renames = {
49
+ "MotorLimitsException": MotorLimitsError,
50
+ }
51
+ rename = renames.get(name)
52
+ if rename is not None:
53
+ warnings.warn(
54
+ DeprecationWarning(
55
+ f"{name!r} is deprecated, use {rename.__name__!r} instead"
56
+ ),
57
+ stacklevel=2,
58
+ )
59
+ return rename
60
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
61
+
62
+
44
63
  class OffsetMode(StrictEnum):
45
64
  VARIABLE = "Variable"
46
65
  FROZEN = "Frozen"
@@ -126,7 +145,7 @@ class Motor(
126
145
  not motor_upper_limit >= abs_start_pos >= motor_lower_limit
127
146
  or not motor_upper_limit >= abs_end_pos >= motor_lower_limit
128
147
  ):
129
- raise MotorLimitsException(
148
+ raise MotorLimitsError(
130
149
  f"{self.name} motor trajectory for requested fly/move is from "
131
150
  f"{abs_start_pos}{egu} to "
132
151
  f"{abs_end_pos}{egu} but motor limits are "
@@ -144,7 +163,7 @@ class Motor(
144
163
  self.max_velocity.get_value(), self.motor_egu.get_value()
145
164
  )
146
165
  if abs(value.velocity) > max_speed:
147
- raise MotorLimitsException(
166
+ raise MotorLimitsError(
148
167
  f"Velocity {abs(value.velocity)} {egu}/s was requested for a motor "
149
168
  f" with max speed of {max_speed} {egu}/s"
150
169
  )
@@ -242,9 +261,11 @@ class Motor(
242
261
  )
243
262
  return Location(setpoint=setpoint, readback=readback)
244
263
 
245
- def subscribe(self, function: Callback[dict[str, Reading[float]]]) -> None:
246
- """Subscribe."""
247
- self.user_readback.subscribe(function)
264
+ def subscribe_reading(self, function: Callback[dict[str, Reading[float]]]) -> None:
265
+ """Subscribe to reading."""
266
+ self.user_readback.subscribe_reading(function)
267
+
268
+ subscribe = subscribe_reading
248
269
 
249
270
  def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None:
250
271
  """Unsubscribe."""
@@ -4,7 +4,7 @@ import numpy as np
4
4
 
5
5
  from ophyd_async.core import Array1D, Device, DeviceVector, StandardReadable
6
6
  from ophyd_async.epics import motor
7
- from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
7
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
8
8
 
9
9
  CS_LETTERS = "ABCUVWXYZ"
10
10
 
@@ -40,11 +40,15 @@ class PmacTrajectoryIO(StandardReadable):
40
40
  for i, letter in enumerate(CS_LETTERS)
41
41
  }
42
42
  )
43
+ self.total_points = epics_signal_r(int, f"{prefix}TotalPoints_RBV")
43
44
  self.points_to_build = epics_signal_rw(int, prefix + "ProfilePointsToBuild")
44
- self.build_profile = epics_signal_rw(bool, prefix + "ProfileBuild")
45
+ self.build_profile = epics_signal_x(prefix + "ProfileBuild")
46
+ self.append_profile = epics_signal_x(prefix + "ProfileAppend")
47
+ # This should be a SignalX, but because it is a Busy record, must
48
+ # be a SignalRW to be waited on in PmacTrajectoryTriggerLogic.
49
+ # TODO: Change record type to bo from busy (https://github.com/DiamondLightSource/pmac/issues/154)
45
50
  self.execute_profile = epics_signal_rw(bool, prefix + "ProfileExecute")
46
- self.scan_percent = epics_signal_r(float, prefix + "TscanPercent_RBV")
47
- self.abort_profile = epics_signal_rw(bool, prefix + "ProfileAbort")
51
+ self.abort_profile = epics_signal_x(prefix + "ProfileAbort")
48
52
  self.profile_cs_name = epics_signal_rw(str, prefix + "ProfileCsName")
49
53
  self.calculate_velocities = epics_signal_rw(bool, prefix + "ProfileCalcVel")
50
54