ophyd-async 0.3a4__py3-none-any.whl → 0.3a6__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 (36) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +6 -1
  3. ophyd_async/core/async_status.py +83 -39
  4. ophyd_async/core/detector.py +23 -29
  5. ophyd_async/core/device.py +32 -11
  6. ophyd_async/core/flyer.py +1 -1
  7. ophyd_async/core/mock_signal_backend.py +14 -15
  8. ophyd_async/core/mock_signal_utils.py +9 -13
  9. ophyd_async/core/signal.py +71 -21
  10. ophyd_async/core/utils.py +30 -0
  11. ophyd_async/epics/areadetector/aravis.py +1 -5
  12. ophyd_async/epics/areadetector/controllers/aravis_controller.py +6 -1
  13. ophyd_async/epics/areadetector/drivers/ad_base.py +1 -7
  14. ophyd_async/epics/areadetector/drivers/aravis_driver.py +2 -120
  15. ophyd_async/epics/areadetector/writers/hdf_writer.py +3 -2
  16. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +0 -2
  17. ophyd_async/epics/areadetector/writers/nd_plugin.py +9 -0
  18. ophyd_async/epics/demo/__init__.py +33 -34
  19. ophyd_async/epics/motion/motor.py +47 -42
  20. ophyd_async/epics/pvi/pvi.py +2 -2
  21. ophyd_async/epics/signal/__init__.py +8 -1
  22. ophyd_async/panda/__init__.py +2 -0
  23. ophyd_async/panda/writers/_hdf_writer.py +4 -4
  24. ophyd_async/plan_stubs/__init__.py +13 -0
  25. ophyd_async/plan_stubs/ensure_connected.py +22 -0
  26. ophyd_async/plan_stubs/fly.py +149 -0
  27. ophyd_async/protocols.py +32 -2
  28. ophyd_async/sim/demo/sim_motor.py +67 -82
  29. {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a6.dist-info}/METADATA +1 -1
  30. {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a6.dist-info}/RECORD +34 -33
  31. ophyd_async/planstubs/__init__.py +0 -5
  32. ophyd_async/planstubs/prepare_trigger_and_dets.py +0 -57
  33. {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a6.dist-info}/LICENSE +0 -0
  34. {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a6.dist-info}/WHEEL +0 -0
  35. {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a6.dist-info}/entry_points.txt +0 -0
  36. {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a6.dist-info}/top_level.txt +0 -0
@@ -21,6 +21,7 @@ from bluesky.protocols import (
21
21
  Location,
22
22
  Movable,
23
23
  Reading,
24
+ Status,
24
25
  Subscribable,
25
26
  )
26
27
 
@@ -31,7 +32,7 @@ from .async_status import AsyncStatus
31
32
  from .device import Device
32
33
  from .signal_backend import SignalBackend
33
34
  from .soft_signal_backend import SoftSignalBackend
34
- from .utils import DEFAULT_TIMEOUT, Callback, T
35
+ from .utils import DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, Callback, T
35
36
 
36
37
 
37
38
  def _add_timeout(func):
@@ -64,7 +65,9 @@ class Signal(Device, Generic[T]):
64
65
  self._initial_backend = self._backend = backend
65
66
  super().__init__(name)
66
67
 
67
- async def connect(self, mock=False, timeout=DEFAULT_TIMEOUT):
68
+ async def connect(
69
+ self, mock=False, timeout=DEFAULT_TIMEOUT, force_reconnect: bool = False
70
+ ):
68
71
  if mock and not isinstance(self._backend, MockSignalBackend):
69
72
  # Using a soft backend, look to the initial value
70
73
  self._backend = MockSignalBackend(
@@ -210,15 +213,14 @@ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
210
213
  self._del_cache(self._get_cache().set_staged(False))
211
214
 
212
215
 
213
- USE_DEFAULT_TIMEOUT = "USE_DEFAULT_TIMEOUT"
214
-
215
-
216
216
  class SignalW(Signal[T], Movable):
217
217
  """Signal that can be set"""
218
218
 
219
- def set(self, value: T, wait=True, timeout=USE_DEFAULT_TIMEOUT) -> AsyncStatus:
219
+ def set(
220
+ self, value: T, wait=True, timeout: CalculatableTimeout = CalculateTimeout
221
+ ) -> AsyncStatus:
220
222
  """Set the value and return a status saying when it's done"""
221
- if timeout is USE_DEFAULT_TIMEOUT:
223
+ if timeout is CalculateTimeout:
222
224
  timeout = self._timeout
223
225
 
224
226
  async def do_set():
@@ -245,9 +247,11 @@ class SignalRW(SignalR[T], SignalW[T], Locatable):
245
247
  class SignalX(Signal):
246
248
  """Signal that puts the default value"""
247
249
 
248
- def trigger(self, wait=True, timeout=USE_DEFAULT_TIMEOUT) -> AsyncStatus:
250
+ def trigger(
251
+ self, wait=True, timeout: CalculatableTimeout = CalculateTimeout
252
+ ) -> AsyncStatus:
249
253
  """Trigger the action and return a status saying when it's done"""
250
- if timeout is USE_DEFAULT_TIMEOUT:
254
+ if timeout is CalculateTimeout:
251
255
  timeout = self._timeout
252
256
  coro = self._backend.put(None, wait=wait, timeout=timeout)
253
257
  return AsyncStatus(coro)
@@ -267,7 +271,7 @@ def soft_signal_r_and_setter(
267
271
  datatype: Optional[Type[T]] = None,
268
272
  initial_value: Optional[T] = None,
269
273
  name: str = "",
270
- ) -> Tuple[SignalR[T], Callable[[T]]]:
274
+ ) -> Tuple[SignalR[T], Callable[[T], None]]:
271
275
  """Returns a tuple of a read-only Signal and a callable through
272
276
  which the signal can be internally modified within the device. Use
273
277
  soft_signal_rw if you want a device that is externally modifiable
@@ -278,6 +282,19 @@ def soft_signal_r_and_setter(
278
282
  return (signal, backend.set_value)
279
283
 
280
284
 
285
+ def _generate_assert_error_msg(
286
+ name: str, expected_result: str, actuall_result: str
287
+ ) -> str:
288
+ WARNING = "\033[93m"
289
+ FAIL = "\033[91m"
290
+ ENDC = "\033[0m"
291
+ return (
292
+ f"Expected {WARNING}{name}{ENDC} to produce"
293
+ + f"\n{FAIL}{actuall_result}{ENDC}"
294
+ + f"\nbut actually got \n{FAIL}{expected_result}{ENDC}"
295
+ )
296
+
297
+
281
298
  async def assert_value(signal: SignalR[T], value: Any) -> None:
282
299
  """Assert a signal's value and compare it an expected signal.
283
300
 
@@ -294,11 +311,14 @@ async def assert_value(signal: SignalR[T], value: Any) -> None:
294
311
  await assert_value(signal, value)
295
312
 
296
313
  """
297
- assert await signal.get_value() == value
314
+ actual_value = await signal.get_value()
315
+ assert actual_value == value, _generate_assert_error_msg(
316
+ signal.name, value, actual_value
317
+ )
298
318
 
299
319
 
300
320
  async def assert_reading(
301
- readable: AsyncReadable, reading: Mapping[str, Reading]
321
+ readable: AsyncReadable, expected_reading: Mapping[str, Reading]
302
322
  ) -> None:
303
323
  """Assert readings from readable.
304
324
 
@@ -316,7 +336,10 @@ async def assert_reading(
316
336
  await assert_reading(readable, reading)
317
337
 
318
338
  """
319
- assert await readable.read() == reading
339
+ actual_reading = await readable.read()
340
+ assert expected_reading == actual_reading, _generate_assert_error_msg(
341
+ readable.name, expected_reading, actual_reading
342
+ )
320
343
 
321
344
 
322
345
  async def assert_configuration(
@@ -339,7 +362,10 @@ async def assert_configuration(
339
362
  await assert_configuration(configurable configuration)
340
363
 
341
364
  """
342
- assert await configurable.read_configuration() == configuration
365
+ actual_configurable = await configurable.read_configuration()
366
+ assert configuration == actual_configurable, _generate_assert_error_msg(
367
+ configurable.name, configuration, actual_configurable
368
+ )
343
369
 
344
370
 
345
371
  def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
@@ -359,11 +385,18 @@ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
359
385
  assert_emitted(docs, start=1, descriptor=1,
360
386
  resource=1, datum=1, event=1, stop=1)
361
387
  """
362
- assert list(docs) == list(numbers)
363
- assert {name: len(d) for name, d in docs.items()} == numbers
364
-
365
-
366
- async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, None]:
388
+ assert list(docs) == list(numbers), _generate_assert_error_msg(
389
+ "documents", list(numbers), list(docs)
390
+ )
391
+ actual_numbers = {name: len(d) for name, d in docs.items()}
392
+ assert actual_numbers == numbers, _generate_assert_error_msg(
393
+ "emitted", numbers, actual_numbers
394
+ )
395
+
396
+
397
+ async def observe_value(
398
+ signal: SignalR[T], timeout: float | None = None, done_status: Status | None = None
399
+ ) -> AsyncGenerator[T, None]:
367
400
  """Subscribe to the value of a signal so it can be iterated from.
368
401
 
369
402
  Parameters
@@ -371,6 +404,12 @@ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, N
371
404
  signal:
372
405
  Call subscribe_value on this at the start, and clear_sub on it at the
373
406
  end
407
+ timeout:
408
+ If given, how long to wait for each updated value in seconds. If an update
409
+ is not produced in this time then raise asyncio.TimeoutError
410
+ done_status:
411
+ If this status is complete, stop observing and make the iterator return.
412
+ If it raises an exception then this exception will be raised by the iterator.
374
413
 
375
414
  Notes
376
415
  -----
@@ -379,7 +418,8 @@ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, N
379
418
  async for value in observe_value(sig):
380
419
  do_something_with(value)
381
420
  """
382
- q: asyncio.Queue[T] = asyncio.Queue()
421
+
422
+ q: asyncio.Queue[T | Status] = asyncio.Queue()
383
423
  if timeout is None:
384
424
  get_value = q.get
385
425
  else:
@@ -387,10 +427,20 @@ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, N
387
427
  async def get_value():
388
428
  return await asyncio.wait_for(q.get(), timeout)
389
429
 
430
+ if done_status is not None:
431
+ done_status.add_callback(q.put_nowait)
432
+
390
433
  signal.subscribe_value(q.put_nowait)
391
434
  try:
392
435
  while True:
393
- yield await get_value()
436
+ item = await get_value()
437
+ if done_status and item is done_status:
438
+ if exc := done_status.exception():
439
+ raise exc
440
+ else:
441
+ break
442
+ else:
443
+ yield item
394
444
  finally:
395
445
  signal.clear_sub(q.put_nowait)
396
446
 
ophyd_async/core/utils.py CHANGED
@@ -2,13 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ from dataclasses import dataclass
5
6
  from typing import (
6
7
  Awaitable,
7
8
  Callable,
8
9
  Dict,
10
+ Generic,
9
11
  Iterable,
10
12
  List,
11
13
  Optional,
14
+ ParamSpec,
12
15
  Type,
13
16
  TypeVar,
14
17
  Union,
@@ -18,6 +21,7 @@ import numpy as np
18
21
  from bluesky.protocols import Reading
19
22
 
20
23
  T = TypeVar("T")
24
+ P = ParamSpec("P")
21
25
  Callback = Callable[[T], None]
22
26
 
23
27
  #: A function that will be called with the Reading and value when the
@@ -27,6 +31,17 @@ DEFAULT_TIMEOUT = 10.0
27
31
  ErrorText = Union[str, Dict[str, Exception]]
28
32
 
29
33
 
34
+ class CalculateTimeout:
35
+ """Sentinel class used to implement ``myfunc(timeout=CalculateTimeout)``
36
+
37
+ This signifies that the function should calculate a suitable non-zero
38
+ timeout itself
39
+ """
40
+
41
+
42
+ CalculatableTimeout = float | None | Type[CalculateTimeout]
43
+
44
+
30
45
  class NotConnected(Exception):
31
46
  """Exception to be raised if a `Device.connect` is cancelled"""
32
47
 
@@ -77,6 +92,21 @@ class NotConnected(Exception):
77
92
  return self.format_error_string(indent="")
78
93
 
79
94
 
95
+ @dataclass(frozen=True)
96
+ class WatcherUpdate(Generic[T]):
97
+ """A dataclass such that, when expanded, it provides the kwargs for a watcher"""
98
+
99
+ current: T
100
+ initial: T
101
+ target: T
102
+ name: str | None = None
103
+ unit: str | None = None
104
+ precision: float | None = None
105
+ fraction: float | None = None
106
+ time_elapsed: float | None = None
107
+ time_remaining: float | None = None
108
+
109
+
80
110
  async def wait_for_connection(**coros: Awaitable[None]):
81
111
  """Call many underlying signals, accumulating exceptions and returning them
82
112
 
@@ -2,7 +2,7 @@ from typing import get_args
2
2
 
3
3
  from bluesky.protocols import HasHints, Hints
4
4
 
5
- from ophyd_async.core import DirectoryProvider, StandardDetector, TriggerInfo
5
+ from ophyd_async.core import DirectoryProvider, StandardDetector
6
6
  from ophyd_async.epics.areadetector.controllers.aravis_controller import (
7
7
  AravisController,
8
8
  )
@@ -45,10 +45,6 @@ class AravisDetector(StandardDetector, HasHints):
45
45
  name=name,
46
46
  )
47
47
 
48
- async def _prepare(self, value: TriggerInfo) -> None:
49
- await self.drv.fetch_deadtime()
50
- await super()._prepare(value)
51
-
52
48
  def get_external_trigger_gpio(self):
53
49
  return self._controller.gpio_number
54
50
 
@@ -14,6 +14,11 @@ from ophyd_async.epics.areadetector.drivers.aravis_driver import (
14
14
  )
15
15
  from ophyd_async.epics.areadetector.utils import ImageMode, stop_busy_record
16
16
 
17
+ # The deadtime of an ADaravis controller varies depending on the exact model of camera.
18
+ # Ideally we would maximize performance by dynamically retrieving the deadtime at
19
+ # runtime. See https://github.com/bluesky/ophyd-async/issues/308
20
+ _HIGHEST_POSSIBLE_DEADTIME = 1961e-6
21
+
17
22
 
18
23
  class AravisController(DetectorControl):
19
24
  GPIO_NUMBER = Literal[1, 2, 3, 4]
@@ -23,7 +28,7 @@ class AravisController(DetectorControl):
23
28
  self.gpio_number = gpio_number
24
29
 
25
30
  def get_deadtime(self, exposure: float) -> float:
26
- return self._drv.dead_time or 0
31
+ return _HIGHEST_POSSIBLE_DEADTIME
27
32
 
28
33
  async def arm(
29
34
  self,
@@ -9,7 +9,7 @@ from ophyd_async.core import (
9
9
  set_and_wait_for_value,
10
10
  )
11
11
 
12
- from ...signal.signal import epics_signal_r, epics_signal_rw, epics_signal_rw_rbv
12
+ from ...signal.signal import epics_signal_r, epics_signal_rw_rbv
13
13
  from ..utils import ImageMode
14
14
  from ..writers.nd_plugin import NDArrayBase
15
15
 
@@ -43,18 +43,12 @@ DEFAULT_GOOD_STATES: FrozenSet[DetectorState] = frozenset(
43
43
  class ADBase(NDArrayBase):
44
44
  def __init__(self, prefix: str, name: str = "") -> None:
45
45
  # Define some signals
46
- self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire")
47
46
  self.acquire_time = epics_signal_rw_rbv(float, prefix + "AcquireTime")
48
47
  self.num_images = epics_signal_rw_rbv(int, prefix + "NumImages")
49
48
  self.image_mode = epics_signal_rw_rbv(ImageMode, prefix + "ImageMode")
50
- self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter")
51
- self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
52
- self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
53
49
  self.detector_state = epics_signal_r(
54
50
  DetectorState, prefix + "DetectorState_RBV"
55
51
  )
56
- # There is no _RBV for this one
57
- self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins")
58
52
  super().__init__(prefix, name=name)
59
53
 
60
54
 
@@ -1,8 +1,8 @@
1
1
  from enum import Enum
2
- from typing import Callable, Dict, Literal, Optional, Tuple
2
+ from typing import Literal
3
3
 
4
4
  from ophyd_async.epics.areadetector.drivers import ADBase
5
- from ophyd_async.epics.signal.signal import epics_signal_r, epics_signal_rw_rbv
5
+ from ophyd_async.epics.signal.signal import epics_signal_rw_rbv
6
6
 
7
7
 
8
8
  class AravisTriggerMode(str, Enum):
@@ -22,113 +22,6 @@ class AravisTriggerMode(str, Enum):
22
22
  AravisTriggerSource = Literal["Freerun", "Line1", "Line2", "Line3", "Line4"]
23
23
 
24
24
 
25
- def _reverse_lookup(
26
- model_deadtimes: Dict[float, Tuple[str, ...]],
27
- ) -> Callable[[str], float]:
28
- def inner(pixel_format: str, model_name: str) -> float:
29
- for deadtime, pixel_formats in model_deadtimes.items():
30
- if pixel_format in pixel_formats:
31
- return deadtime
32
- raise ValueError(
33
- f"Model {model_name} does not have a defined deadtime "
34
- f"for pixel format {pixel_format}"
35
- )
36
-
37
- return inner
38
-
39
-
40
- _deadtimes: Dict[str, Callable[[str, str], float]] = {
41
- # cite: https://cdn.alliedvision.com/fileadmin/content/documents/products/cameras/Manta/techman/Manta_TechMan.pdf retrieved 2024-04-05 # noqa: E501
42
- "Manta G-125": lambda _, __: 63e-6,
43
- "Manta G-145": lambda _, __: 106e-6,
44
- "Manta G-235": _reverse_lookup(
45
- {
46
- 118e-6: (
47
- "Mono8",
48
- "Mono12Packed",
49
- "BayerRG8",
50
- "BayerRG12",
51
- "BayerRG12Packed",
52
- "YUV411Packed",
53
- ),
54
- 256e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
55
- 390e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
56
- }
57
- ),
58
- "Manta G-895": _reverse_lookup(
59
- {
60
- 404e-6: (
61
- "Mono8",
62
- "Mono12Packed",
63
- "BayerRG8",
64
- "BayerRG12Packed",
65
- "YUV411Packed",
66
- ),
67
- 542e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
68
- 822e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
69
- }
70
- ),
71
- "Manta G-2460": _reverse_lookup(
72
- {
73
- 979e-6: (
74
- "Mono8",
75
- "Mono12Packed",
76
- "BayerRG8",
77
- "BayerRG12Packed",
78
- "YUV411Packed",
79
- ),
80
- 1304e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
81
- 1961e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
82
- }
83
- ),
84
- # cite: https://cdn.alliedvision.com/fileadmin/content/documents/products/cameras/various/appnote/GigE/GigE-Cameras_AppNote_PIV-Min-Time-Between-Exposures.pdf retrieved 2024-04-05 # noqa: E501
85
- "Manta G-609": lambda _, __: 47e-6,
86
- # cite: https://cdn.alliedvision.com/fileadmin/content/documents/products/cameras/Mako/techman/Mako_TechMan_en.pdf retrieved 2024-04-05 # noqa: E501
87
- "Mako G-040": _reverse_lookup(
88
- {
89
- 101e-6: (
90
- "Mono8",
91
- "Mono12Packed",
92
- "BayerRG8",
93
- "BayerRG12Packed",
94
- "YUV411Packed",
95
- ),
96
- 140e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
97
- 217e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
98
- }
99
- ),
100
- "Mako G-125": lambda _, __: 70e-6,
101
- # Assume 12 bits: 10 bits = 275e-6
102
- "Mako G-234": _reverse_lookup(
103
- {
104
- 356e-6: (
105
- "Mono8",
106
- "BayerRG8",
107
- "BayerRG12",
108
- "BayerRG12Packed",
109
- "YUV411Packed",
110
- "YUV422Packed",
111
- ),
112
- # Assume 12 bits: 10 bits = 563e-6
113
- 726e-6: ("RGB8Packed", "BRG8Packed", "YUV444Packed"),
114
- }
115
- ),
116
- "Mako G-507": _reverse_lookup(
117
- {
118
- 270e-6: (
119
- "Mono8",
120
- "Mono12Packed",
121
- "BayerRG8",
122
- "BayerRG12Packed",
123
- "YUV411Packed",
124
- ),
125
- 363e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
126
- 554e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
127
- }
128
- ),
129
- }
130
-
131
-
132
25
  class AravisDriver(ADBase):
133
26
  # If instantiating a new instance, ensure it is supported in the _deadtimes dict
134
27
  """Generic Driver supporting the Manta and Mako drivers.
@@ -142,15 +35,4 @@ class AravisDriver(ADBase):
142
35
  AravisTriggerMode, prefix + "TriggerMode"
143
36
  )
144
37
  self.trigger_source = epics_signal_rw_rbv(str, prefix + "TriggerSource")
145
- self.model = epics_signal_r(str, prefix + "Model_RBV")
146
- self.pixel_format = epics_signal_rw_rbv(str, prefix + "PixelFormat")
147
- self.dead_time: Optional[float] = None
148
38
  super().__init__(prefix, name=name)
149
-
150
- async def fetch_deadtime(self) -> None:
151
- # All known in-use version B/C have same deadtime as non-B/C
152
- model: str = (await self.model.get_value()).removesuffix("B").removesuffix("C")
153
- if model not in _deadtimes:
154
- raise ValueError(f"Model {model} does not have defined deadtimes")
155
- pixel_format: str = await self.pixel_format.get_value()
156
- self.dead_time = _deadtimes.get(model)(pixel_format, model)
@@ -43,12 +43,13 @@ class HDFWriter(DetectorWriter):
43
43
  async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
44
44
  self._file = None
45
45
  info = self._directory_provider()
46
+ file_path = str(info.root / info.resource_dir)
46
47
  await asyncio.gather(
47
48
  self.hdf.num_extra_dims.set(0),
48
49
  self.hdf.lazy_open.set(True),
49
50
  self.hdf.swmr_mode.set(True),
50
51
  # See https://github.com/bluesky/ophyd-async/issues/122
51
- self.hdf.file_path.set(str(info.root / info.resource_dir)),
52
+ self.hdf.file_path.set(file_path),
52
53
  self.hdf.file_name.set(f"{info.prefix}{self.hdf.name}{info.suffix}"),
53
54
  self.hdf.file_template.set("%s/%s.h5"),
54
55
  self.hdf.file_write_mode.set(FileWriteMode.stream),
@@ -59,7 +60,7 @@ class HDFWriter(DetectorWriter):
59
60
 
60
61
  assert (
61
62
  await self.hdf.file_path_exists.get_value()
62
- ), f"File path {self.hdf.file_path.get_value()} for hdf plugin does not exist"
63
+ ), f"File path {file_path} for hdf plugin does not exist"
63
64
 
64
65
  # Overwrite num_capture to go forever
65
66
  await self.hdf.num_capture.set(0)
@@ -36,7 +36,5 @@ class NDFileHDF(NDPluginBase):
36
36
  self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen")
37
37
  self.capture = epics_signal_rw_rbv(bool, prefix + "Capture")
38
38
  self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
39
- self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV")
40
- self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV")
41
39
  self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName")
42
40
  super().__init__(prefix, name)
@@ -14,6 +14,13 @@ class NDArrayBase(Device):
14
14
  def __init__(self, prefix: str, name: str = "") -> None:
15
15
  self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV")
16
16
  self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile")
17
+ self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire")
18
+ self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
19
+ self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
20
+ self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter")
21
+ # There is no _RBV for this one
22
+ self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins")
23
+
17
24
  super().__init__(name=name)
18
25
 
19
26
 
@@ -22,6 +29,8 @@ class NDPluginBase(NDArrayBase):
22
29
  self.nd_array_port = epics_signal_rw_rbv(str, prefix + "NDArrayPort")
23
30
  self.enable_callback = epics_signal_rw_rbv(Callback, prefix + "EnableCallbacks")
24
31
  self.nd_array_address = epics_signal_rw_rbv(int, prefix + "NDArrayAddress")
32
+ self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV")
33
+ self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV")
25
34
  super().__init__(prefix, name)
26
35
 
27
36
 
@@ -6,23 +6,28 @@ import random
6
6
  import string
7
7
  import subprocess
8
8
  import sys
9
- import time
10
9
  from enum import Enum
11
10
  from pathlib import Path
12
- from typing import Callable, List, Optional
13
11
 
14
12
  import numpy as np
15
13
  from bluesky.protocols import Movable, Stoppable
16
14
 
17
15
  from ophyd_async.core import (
18
- AsyncStatus,
19
16
  ConfigSignal,
20
17
  Device,
21
18
  DeviceVector,
22
19
  HintedSignal,
23
20
  StandardReadable,
21
+ WatchableAsyncStatus,
24
22
  observe_value,
25
23
  )
24
+ from ophyd_async.core.async_status import AsyncStatus
25
+ from ophyd_async.core.utils import (
26
+ DEFAULT_TIMEOUT,
27
+ CalculatableTimeout,
28
+ CalculateTimeout,
29
+ WatcherUpdate,
30
+ )
26
31
 
27
32
  from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
28
33
 
@@ -66,11 +71,9 @@ class Mover(StandardReadable, Movable, Stoppable):
66
71
  # Define some signals
67
72
  with self.add_children_as_readables(HintedSignal):
68
73
  self.readback = epics_signal_r(float, prefix + "Readback")
69
-
70
74
  with self.add_children_as_readables(ConfigSignal):
71
75
  self.velocity = epics_signal_rw(float, prefix + "Velocity")
72
76
  self.units = epics_signal_r(str, prefix + "Readback.EGU")
73
-
74
77
  self.setpoint = epics_signal_rw(float, prefix + "Setpoint")
75
78
  self.precision = epics_signal_r(int, prefix + "Readback.PREC")
76
79
  # Signals that collide with standard methods should have a trailing underscore
@@ -85,47 +88,43 @@ class Mover(StandardReadable, Movable, Stoppable):
85
88
  # Readback should be named the same as its parent in read()
86
89
  self.readback.set_name(name)
87
90
 
88
- async def _move(self, new_position: float, watchers: List[Callable] = []):
91
+ @WatchableAsyncStatus.wrap
92
+ async def set(
93
+ self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
94
+ ):
89
95
  self._set_success = True
90
- # time.monotonic won't go backwards in case of NTP corrections
91
- start = time.monotonic()
92
- old_position, units, precision = await asyncio.gather(
96
+ old_position, units, precision, velocity = await asyncio.gather(
93
97
  self.setpoint.get_value(),
94
98
  self.units.get_value(),
95
99
  self.precision.get_value(),
100
+ self.velocity.get_value(),
96
101
  )
102
+ if timeout is CalculateTimeout:
103
+ assert velocity > 0, "Mover has zero velocity"
104
+ timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
105
+ # Make an Event that will be set on completion, and a Status that will
106
+ # error if not done in time
107
+ done = asyncio.Event()
108
+ done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout))
97
109
  # Wait for the value to set, but don't wait for put completion callback
98
110
  await self.setpoint.set(new_position, wait=False)
99
- async for current_position in observe_value(self.readback):
100
- for watcher in watchers:
101
- watcher(
102
- name=self.name,
103
- current=current_position,
104
- initial=old_position,
105
- target=new_position,
106
- unit=units,
107
- precision=precision,
108
- time_elapsed=time.monotonic() - start,
109
- )
111
+ async for current_position in observe_value(
112
+ self.readback, done_status=done_status
113
+ ):
114
+ yield WatcherUpdate(
115
+ current=current_position,
116
+ initial=old_position,
117
+ target=new_position,
118
+ name=self.name,
119
+ unit=units,
120
+ precision=precision,
121
+ )
110
122
  if np.isclose(current_position, new_position):
123
+ done.set()
111
124
  break
112
125
  if not self._set_success:
113
126
  raise RuntimeError("Motor was stopped")
114
127
 
115
- def move(self, new_position: float, timeout: Optional[float] = None):
116
- """Commandline only synchronous move of a Motor"""
117
- from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
118
-
119
- if in_bluesky_event_loop():
120
- raise RuntimeError("Will deadlock run engine if run in a plan")
121
- call_in_bluesky_event_loop(self._move(new_position), timeout) # type: ignore
122
-
123
- # TODO: this fails if we call from the cli, but works if we "ipython await" it
124
- def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus:
125
- watchers: List[Callable] = []
126
- coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout)
127
- return AsyncStatus(coro, watchers)
128
-
129
128
  async def stop(self, success=True):
130
129
  self._set_success = success
131
130
  status = self.stop_.trigger()