ophyd-async 0.5.1__py3-none-any.whl → 0.6.0__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 (73) hide show
  1. ophyd_async/__init__.py +10 -1
  2. ophyd_async/__main__.py +12 -4
  3. ophyd_async/_version.py +2 -2
  4. ophyd_async/core/__init__.py +15 -6
  5. ophyd_async/core/_detector.py +72 -63
  6. ophyd_async/core/_device.py +13 -15
  7. ophyd_async/core/_device_save_loader.py +30 -19
  8. ophyd_async/core/_flyer.py +6 -4
  9. ophyd_async/core/_hdf_dataset.py +8 -9
  10. ophyd_async/core/_log.py +3 -1
  11. ophyd_async/core/_mock_signal_backend.py +11 -9
  12. ophyd_async/core/_mock_signal_utils.py +8 -5
  13. ophyd_async/core/_protocol.py +7 -7
  14. ophyd_async/core/_providers.py +17 -13
  15. ophyd_async/core/_readable.py +30 -22
  16. ophyd_async/core/_signal.py +53 -52
  17. ophyd_async/core/_signal_backend.py +20 -7
  18. ophyd_async/core/_soft_signal_backend.py +62 -32
  19. ophyd_async/core/_status.py +18 -4
  20. ophyd_async/core/_table.py +63 -0
  21. ophyd_async/core/_utils.py +24 -28
  22. ophyd_async/epics/adaravis/_aravis.py +1 -1
  23. ophyd_async/epics/adaravis/_aravis_controller.py +17 -16
  24. ophyd_async/epics/adaravis/_aravis_io.py +2 -1
  25. ophyd_async/epics/adcore/__init__.py +2 -2
  26. ophyd_async/epics/adcore/_core_io.py +2 -0
  27. ophyd_async/epics/adcore/_core_logic.py +9 -7
  28. ophyd_async/epics/adcore/_hdf_writer.py +26 -21
  29. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  30. ophyd_async/epics/adcore/_utils.py +5 -6
  31. ophyd_async/epics/adkinetix/_kinetix.py +1 -1
  32. ophyd_async/epics/adkinetix/_kinetix_controller.py +19 -14
  33. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  34. ophyd_async/epics/adpilatus/_pilatus_controller.py +18 -16
  35. ophyd_async/epics/adsimdetector/_sim.py +7 -6
  36. ophyd_async/epics/adsimdetector/_sim_controller.py +20 -15
  37. ophyd_async/epics/advimba/_vimba.py +1 -1
  38. ophyd_async/epics/advimba/_vimba_controller.py +21 -16
  39. ophyd_async/epics/demo/_mover.py +4 -5
  40. ophyd_async/epics/demo/sensor.db +0 -1
  41. ophyd_async/epics/eiger/__init__.py +5 -0
  42. ophyd_async/epics/eiger/_eiger.py +43 -0
  43. ophyd_async/epics/eiger/_eiger_controller.py +66 -0
  44. ophyd_async/epics/eiger/_eiger_io.py +42 -0
  45. ophyd_async/epics/eiger/_odin_io.py +126 -0
  46. ophyd_async/epics/motor.py +9 -11
  47. ophyd_async/epics/pvi/_pvi.py +30 -33
  48. ophyd_async/epics/signal/_aioca.py +55 -25
  49. ophyd_async/epics/signal/_common.py +3 -10
  50. ophyd_async/epics/signal/_epics_transport.py +11 -8
  51. ophyd_async/epics/signal/_p4p.py +79 -30
  52. ophyd_async/epics/signal/_signal.py +6 -8
  53. ophyd_async/fastcs/panda/__init__.py +0 -6
  54. ophyd_async/fastcs/panda/_control.py +14 -15
  55. ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
  56. ophyd_async/fastcs/panda/_table.py +111 -138
  57. ophyd_async/fastcs/panda/_trigger.py +1 -2
  58. ophyd_async/fastcs/panda/_utils.py +3 -2
  59. ophyd_async/fastcs/panda/_writer.py +28 -13
  60. ophyd_async/plan_stubs/_fly.py +16 -16
  61. ophyd_async/plan_stubs/_nd_attributes.py +12 -6
  62. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
  63. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +24 -20
  64. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
  65. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
  66. ophyd_async/sim/demo/_sim_motor.py +2 -1
  67. {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/METADATA +46 -45
  68. ophyd_async-0.6.0.dist-info/RECORD +96 -0
  69. {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/WHEEL +1 -1
  70. ophyd_async-0.5.1.dist-info/RECORD +0 -90
  71. {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/LICENSE +0 -0
  72. {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/entry_points.txt +0 -0
  73. {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  import asyncio
2
- from typing import Optional, Set
3
2
 
4
3
  from ophyd_async.core import (
5
4
  DEFAULT_TIMEOUT,
6
- AsyncStatus,
7
5
  DetectorControl,
8
6
  DetectorTrigger,
9
7
  )
8
+ from ophyd_async.core._detector import TriggerInfo
9
+ from ophyd_async.core._status import AsyncStatus
10
10
  from ophyd_async.epics import adcore
11
11
 
12
12
 
@@ -14,32 +14,37 @@ class SimController(DetectorControl):
14
14
  def __init__(
15
15
  self,
16
16
  driver: adcore.ADBaseIO,
17
- good_states: Set[adcore.DetectorState] = set(adcore.DEFAULT_GOOD_STATES),
17
+ good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
18
18
  ) -> None:
19
19
  self.driver = driver
20
20
  self.good_states = good_states
21
+ self.frame_timeout: float
22
+ self._arm_status: AsyncStatus | None = None
21
23
 
22
- def get_deadtime(self, exposure: float) -> float:
24
+ def get_deadtime(self, exposure: float | None) -> float:
23
25
  return 0.002
24
26
 
25
- async def arm(
26
- self,
27
- num: int,
28
- trigger: DetectorTrigger = DetectorTrigger.internal,
29
- exposure: Optional[float] = None,
30
- ) -> AsyncStatus:
27
+ async def prepare(self, trigger_info: TriggerInfo):
31
28
  assert (
32
- trigger == DetectorTrigger.internal
29
+ trigger_info.trigger == DetectorTrigger.internal
33
30
  ), "fly scanning (i.e. external triggering) is not supported for this device"
34
- frame_timeout = DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
31
+ self.frame_timeout = (
32
+ DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
33
+ )
35
34
  await asyncio.gather(
36
- self.driver.num_images.set(num),
35
+ self.driver.num_images.set(trigger_info.number),
37
36
  self.driver.image_mode.set(adcore.ImageMode.multiple),
38
37
  )
39
- return await adcore.start_acquiring_driver_and_ensure_status(
40
- self.driver, good_states=self.good_states, timeout=frame_timeout
38
+
39
+ async def arm(self):
40
+ self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
41
+ self.driver, good_states=self.good_states, timeout=self.frame_timeout
41
42
  )
42
43
 
44
+ async def wait_for_idle(self):
45
+ if self._arm_status:
46
+ await self._arm_status
47
+
43
48
  async def disarm(self):
44
49
  # We can't use caput callback as we already used it in arm() and we can't have
45
50
  # 2 or they will deadlock
@@ -32,7 +32,7 @@ class VimbaDetector(StandardDetector, HasHints):
32
32
  self.hdf,
33
33
  path_provider,
34
34
  lambda: self.name,
35
- adcore.ADBaseShapeProvider(self.drv),
35
+ adcore.ADBaseDatasetDescriber(self.drv),
36
36
  ),
37
37
  config_sigs=(self.drv.acquire_time,),
38
38
  name=name,
@@ -1,7 +1,8 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
- from ophyd_async.core import AsyncStatus, DetectorControl, DetectorTrigger
3
+ from ophyd_async.core import DetectorControl, DetectorTrigger
4
+ from ophyd_async.core._detector import TriggerInfo
5
+ from ophyd_async.core._status import AsyncStatus
5
6
  from ophyd_async.epics import adcore
6
7
 
7
8
  from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
@@ -27,32 +28,36 @@ class VimbaController(DetectorControl):
27
28
  driver: VimbaDriverIO,
28
29
  ) -> None:
29
30
  self._drv = driver
31
+ self._arm_status: AsyncStatus | None = None
30
32
 
31
- def get_deadtime(self, exposure: float) -> float:
33
+ def get_deadtime(self, exposure: float | None) -> float:
32
34
  return 0.001
33
35
 
34
- async def arm(
35
- self,
36
- num: int,
37
- trigger: DetectorTrigger = DetectorTrigger.internal,
38
- exposure: Optional[float] = None,
39
- ) -> AsyncStatus:
36
+ async def prepare(self, trigger_info: TriggerInfo):
40
37
  await asyncio.gather(
41
- self._drv.trigger_mode.set(TRIGGER_MODE[trigger]),
42
- self._drv.exposure_mode.set(EXPOSE_OUT_MODE[trigger]),
43
- self._drv.num_images.set(num),
38
+ self._drv.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
39
+ self._drv.exposure_mode.set(EXPOSE_OUT_MODE[trigger_info.trigger]),
40
+ self._drv.num_images.set(trigger_info.number),
44
41
  self._drv.image_mode.set(adcore.ImageMode.multiple),
45
42
  )
46
- if exposure is not None and trigger not in [
43
+ if trigger_info.livetime is not None and trigger_info.trigger not in [
47
44
  DetectorTrigger.variable_gate,
48
45
  DetectorTrigger.constant_gate,
49
46
  ]:
50
- await self._drv.acquire_time.set(exposure)
51
- if trigger != DetectorTrigger.internal:
47
+ await self._drv.acquire_time.set(trigger_info.livetime)
48
+ if trigger_info.trigger != DetectorTrigger.internal:
52
49
  self._drv.trigger_source.set(VimbaTriggerSource.line1)
53
50
  else:
54
51
  self._drv.trigger_source.set(VimbaTriggerSource.freerun)
55
- return await adcore.start_acquiring_driver_and_ensure_status(self._drv)
52
+
53
+ async def arm(self):
54
+ self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
55
+ self._drv
56
+ )
57
+
58
+ async def wait_for_idle(self):
59
+ if self._arm_status:
60
+ await self._arm_status
56
61
 
57
62
  async def disarm(self):
58
63
  await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
@@ -4,10 +4,10 @@ import numpy as np
4
4
  from bluesky.protocols import Movable, Stoppable
5
5
 
6
6
  from ophyd_async.core import (
7
+ CALCULATE_TIMEOUT,
7
8
  DEFAULT_TIMEOUT,
8
9
  AsyncStatus,
9
10
  CalculatableTimeout,
10
- CalculateTimeout,
11
11
  ConfigSignal,
12
12
  Device,
13
13
  HintedSignal,
@@ -44,9 +44,8 @@ class Mover(StandardReadable, Movable, Stoppable):
44
44
  self.readback.set_name(name)
45
45
 
46
46
  @WatchableAsyncStatus.wrap
47
- async def set(
48
- self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
49
- ):
47
+ async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
48
+ new_position = value
50
49
  self._set_success = True
51
50
  old_position, units, precision, velocity = await asyncio.gather(
52
51
  self.setpoint.get_value(),
@@ -54,7 +53,7 @@ class Mover(StandardReadable, Movable, Stoppable):
54
53
  self.precision.get_value(),
55
54
  self.velocity.get_value(),
56
55
  )
57
- if timeout is CalculateTimeout:
56
+ if timeout == CALCULATE_TIMEOUT:
58
57
  assert velocity > 0, "Mover has zero velocity"
59
58
  timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
60
59
  # Make an Event that will be set on completion, and a Status that will
@@ -17,4 +17,3 @@ record(calc, "$(P)Value") {
17
17
  field(EGU, "$(EGU=cts/s)")
18
18
  field(PREC, "$(PREC=3)")
19
19
  }
20
-
@@ -0,0 +1,5 @@
1
+ from ._eiger import EigerDetector, EigerTriggerInfo
2
+ from ._eiger_controller import EigerController
3
+ from ._eiger_io import EigerDriverIO
4
+
5
+ __all__ = ["EigerDetector", "EigerController", "EigerDriverIO", "EigerTriggerInfo"]
@@ -0,0 +1,43 @@
1
+ from pydantic import Field
2
+
3
+ from ophyd_async.core import AsyncStatus, PathProvider, StandardDetector
4
+ from ophyd_async.core._detector import TriggerInfo
5
+
6
+ from ._eiger_controller import EigerController
7
+ from ._eiger_io import EigerDriverIO
8
+ from ._odin_io import Odin, OdinWriter
9
+
10
+
11
+ class EigerTriggerInfo(TriggerInfo):
12
+ energy_ev: float = Field(gt=0)
13
+
14
+
15
+ class EigerDetector(StandardDetector):
16
+ """
17
+ Ophyd-async implementation of an Eiger Detector.
18
+ """
19
+
20
+ _controller: EigerController
21
+ _writer: Odin
22
+
23
+ def __init__(
24
+ self,
25
+ prefix: str,
26
+ path_provider: PathProvider,
27
+ drv_suffix="-EA-EIGER-01:",
28
+ hdf_suffix="-EA-ODIN-01:",
29
+ name="",
30
+ ):
31
+ self.drv = EigerDriverIO(prefix + drv_suffix)
32
+ self.odin = Odin(prefix + hdf_suffix + "FP:")
33
+
34
+ super().__init__(
35
+ EigerController(self.drv),
36
+ OdinWriter(path_provider, lambda: self.name, self.odin),
37
+ name=name,
38
+ )
39
+
40
+ @AsyncStatus.wrap
41
+ async def prepare(self, value: EigerTriggerInfo) -> None: # type: ignore
42
+ await self._controller.set_energy(value.energy_ev)
43
+ await super().prepare(value)
@@ -0,0 +1,66 @@
1
+ import asyncio
2
+
3
+ from ophyd_async.core import (
4
+ DEFAULT_TIMEOUT,
5
+ DetectorControl,
6
+ DetectorTrigger,
7
+ set_and_wait_for_other_value,
8
+ )
9
+ from ophyd_async.core._detector import TriggerInfo
10
+
11
+ from ._eiger_io import EigerDriverIO, EigerTriggerMode
12
+
13
+ EIGER_TRIGGER_MODE_MAP = {
14
+ DetectorTrigger.internal: EigerTriggerMode.internal,
15
+ DetectorTrigger.constant_gate: EigerTriggerMode.gate,
16
+ DetectorTrigger.variable_gate: EigerTriggerMode.gate,
17
+ DetectorTrigger.edge_trigger: EigerTriggerMode.edge,
18
+ }
19
+
20
+
21
+ class EigerController(DetectorControl):
22
+ def __init__(
23
+ self,
24
+ driver: EigerDriverIO,
25
+ ) -> None:
26
+ self._drv = driver
27
+
28
+ def get_deadtime(self, exposure: float | None) -> float:
29
+ # See https://media.dectris.com/filer_public/30/14/3014704e-5f3b-43ba-8ccf-8ef720e60d2a/240202_usermanual_eiger2.pdf
30
+ return 0.0001
31
+
32
+ async def set_energy(self, energy: float, tolerance: float = 0.1):
33
+ """Changing photon energy takes some time so only do so if the current energy is
34
+ outside the tolerance."""
35
+ current_energy = await self._drv.photon_energy.get_value()
36
+ if abs(current_energy - energy) > tolerance:
37
+ await self._drv.photon_energy.set(energy)
38
+
39
+ async def prepare(self, trigger_info: TriggerInfo):
40
+ coros = [
41
+ self._drv.trigger_mode.set(
42
+ EIGER_TRIGGER_MODE_MAP[trigger_info.trigger].value
43
+ ),
44
+ self._drv.num_images.set(trigger_info.number),
45
+ ]
46
+ if trigger_info.livetime is not None:
47
+ coros.extend(
48
+ [
49
+ self._drv.acquire_time.set(trigger_info.livetime),
50
+ self._drv.acquire_period.set(trigger_info.livetime),
51
+ ]
52
+ )
53
+ await asyncio.gather(*coros)
54
+
55
+ async def arm(self):
56
+ # TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
57
+ self._arm_status = set_and_wait_for_other_value(
58
+ self._drv.arm, 1, self._drv.state, "ready", timeout=DEFAULT_TIMEOUT
59
+ )
60
+
61
+ async def wait_for_idle(self):
62
+ if self._arm_status:
63
+ await self._arm_status
64
+
65
+ async def disarm(self):
66
+ await self._drv.disarm.set(1)
@@ -0,0 +1,42 @@
1
+ from enum import Enum
2
+
3
+ from ophyd_async.core import Device
4
+ from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw_rbv, epics_signal_w
5
+
6
+
7
+ class EigerTriggerMode(str, Enum):
8
+ internal = "ints"
9
+ edge = "exts"
10
+ gate = "exte"
11
+
12
+
13
+ class EigerDriverIO(Device):
14
+ def __init__(self, prefix: str, name: str = "") -> None:
15
+ self.bit_depth = epics_signal_r(int, f"{prefix}BitDepthReadout")
16
+ self.stale_parameters = epics_signal_r(bool, f"{prefix}StaleParameters")
17
+ self.state = epics_signal_r(str, f"{prefix}DetectorState")
18
+ self.roi_mode = epics_signal_rw_rbv(str, f"{prefix}RoiMode")
19
+
20
+ self.acquire_time = epics_signal_rw_rbv(float, f"{prefix}CountTime")
21
+ self.acquire_period = epics_signal_rw_rbv(float, f"{prefix}FrameTime")
22
+
23
+ self.num_images = epics_signal_rw_rbv(int, f"{prefix}Nimages")
24
+ self.num_triggers = epics_signal_rw_rbv(int, f"{prefix}Ntrigger")
25
+
26
+ # TODO: Should be EigerTriggerMode enum, see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
27
+ self.trigger_mode = epics_signal_rw_rbv(str, f"{prefix}TriggerMode")
28
+
29
+ self.arm = epics_signal_w(int, f"{prefix}Arm")
30
+ self.disarm = epics_signal_w(int, f"{prefix}Disarm")
31
+ self.abort = epics_signal_w(int, f"{prefix}Abort")
32
+
33
+ self.beam_centre_x = epics_signal_rw_rbv(float, f"{prefix}BeamCenterX")
34
+ self.beam_centre_y = epics_signal_rw_rbv(float, f"{prefix}BeamCenterY")
35
+
36
+ self.det_distance = epics_signal_rw_rbv(float, f"{prefix}DetectorDistance")
37
+ self.omega_start = epics_signal_rw_rbv(float, f"{prefix}OmegaStart")
38
+ self.omega_increment = epics_signal_rw_rbv(float, f"{prefix}OmegaIncrement")
39
+
40
+ self.photon_energy = epics_signal_rw_rbv(float, f"{prefix}PhotonEnergy")
41
+
42
+ super().__init__(name)
@@ -0,0 +1,126 @@
1
+ import asyncio
2
+ from collections.abc import AsyncGenerator, AsyncIterator
3
+ from enum import Enum
4
+
5
+ from bluesky.protocols import StreamAsset
6
+ from event_model import DataKey
7
+
8
+ from ophyd_async.core import (
9
+ DEFAULT_TIMEOUT,
10
+ DetectorWriter,
11
+ Device,
12
+ DeviceVector,
13
+ NameProvider,
14
+ PathProvider,
15
+ observe_value,
16
+ set_and_wait_for_value,
17
+ )
18
+ from ophyd_async.epics.signal import (
19
+ epics_signal_r,
20
+ epics_signal_rw,
21
+ epics_signal_rw_rbv,
22
+ )
23
+
24
+
25
+ class Writing(str, Enum):
26
+ ON = "ON"
27
+ OFF = "OFF"
28
+
29
+
30
+ class OdinNode(Device):
31
+ def __init__(self, prefix: str, name: str = "") -> None:
32
+ self.writing = epics_signal_r(Writing, f"{prefix}HDF:Writing")
33
+ self.connected = epics_signal_r(bool, f"{prefix}Connected")
34
+
35
+ super().__init__(name)
36
+
37
+
38
+ class Odin(Device):
39
+ def __init__(self, prefix: str, name: str = "") -> None:
40
+ self.nodes = DeviceVector({i: OdinNode(f"{prefix}FP{i}:") for i in range(4)})
41
+
42
+ self.capture = epics_signal_rw(
43
+ Writing, f"{prefix}Writing", f"{prefix}ConfigHdfWrite"
44
+ )
45
+ self.num_captured = epics_signal_r(int, f"{prefix}FramesWritten")
46
+ self.num_to_capture = epics_signal_rw_rbv(int, f"{prefix}ConfigHdfFrames")
47
+
48
+ self.start_timeout = epics_signal_rw_rbv(int, f"{prefix}TimeoutTimerPeriod")
49
+
50
+ self.image_height = epics_signal_rw_rbv(int, f"{prefix}DatasetDataDims0")
51
+ self.image_width = epics_signal_rw_rbv(int, f"{prefix}DatasetDataDims1")
52
+
53
+ self.num_row_chunks = epics_signal_rw_rbv(int, f"{prefix}DatasetDataChunks1")
54
+ self.num_col_chunks = epics_signal_rw_rbv(int, f"{prefix}DatasetDataChunks2")
55
+
56
+ self.file_path = epics_signal_rw_rbv(str, f"{prefix}ConfigHdfFilePath")
57
+ self.file_name = epics_signal_rw_rbv(str, f"{prefix}ConfigHdfFilePrefix")
58
+
59
+ self.acquisition_id = epics_signal_rw_rbv(
60
+ str, f"{prefix}ConfigHdfAcquisitionId"
61
+ )
62
+
63
+ self.data_type = epics_signal_rw_rbv(str, f"{prefix}DatasetDataDatatype")
64
+
65
+ super().__init__(name)
66
+
67
+
68
+ class OdinWriter(DetectorWriter):
69
+ def __init__(
70
+ self,
71
+ path_provider: PathProvider,
72
+ name_provider: NameProvider,
73
+ odin_driver: Odin,
74
+ ) -> None:
75
+ self._drv = odin_driver
76
+ self._path_provider = path_provider
77
+ self._name_provider = name_provider
78
+ super().__init__()
79
+
80
+ async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
81
+ info = self._path_provider(device_name=self._name_provider())
82
+
83
+ await asyncio.gather(
84
+ self._drv.file_path.set(str(info.directory_path)),
85
+ self._drv.file_name.set(info.filename),
86
+ self._drv.data_type.set(
87
+ "uint16"
88
+ ), # TODO: Get from eiger https://github.com/bluesky/ophyd-async/issues/529
89
+ self._drv.num_to_capture.set(0),
90
+ )
91
+
92
+ await self._drv.capture.set(Writing.ON)
93
+
94
+ return await self._describe()
95
+
96
+ async def _describe(self) -> dict[str, DataKey]:
97
+ data_shape = await asyncio.gather(
98
+ self._drv.image_height.get_value(), self._drv.image_width.get_value()
99
+ )
100
+
101
+ return {
102
+ "data": DataKey(
103
+ source=self._drv.file_name.source,
104
+ shape=data_shape,
105
+ dtype="array",
106
+ # TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529
107
+ dtype_numpy="<u2", # type: ignore
108
+ external="STREAM:",
109
+ )
110
+ }
111
+
112
+ async def observe_indices_written(
113
+ self, timeout=DEFAULT_TIMEOUT
114
+ ) -> AsyncGenerator[int, None]:
115
+ async for num_captured in observe_value(self._drv.num_captured, timeout):
116
+ yield num_captured
117
+
118
+ async def get_indices_written(self) -> int:
119
+ return await self._drv.num_captured.get_value()
120
+
121
+ def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
122
+ # TODO: Correctly return stream https://github.com/bluesky/ophyd-async/issues/530
123
+ raise NotImplementedError()
124
+
125
+ async def close(self) -> None:
126
+ await set_and_wait_for_value(self._drv.capture, Writing.OFF)
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
3
  from bluesky.protocols import (
5
4
  Flyable,
@@ -11,10 +10,10 @@ from bluesky.protocols import (
11
10
  from pydantic import BaseModel, Field
12
11
 
13
12
  from ophyd_async.core import (
13
+ CALCULATE_TIMEOUT,
14
14
  DEFAULT_TIMEOUT,
15
15
  AsyncStatus,
16
16
  CalculatableTimeout,
17
- CalculateTimeout,
18
17
  ConfigSignal,
19
18
  HintedSignal,
20
19
  StandardReadable,
@@ -54,7 +53,7 @@ class FlyMotorInfo(BaseModel):
54
53
 
55
54
  #: Maximum time for the complete motor move, including run up and run down.
56
55
  #: Defaults to `time_for_move` + run up and run down times + 10s.
57
- timeout: CalculatableTimeout = Field(frozen=True, default=CalculateTimeout)
56
+ timeout: CalculatableTimeout = Field(frozen=True, default=CALCULATE_TIMEOUT)
58
57
 
59
58
 
60
59
  class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
@@ -83,13 +82,13 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
83
82
  self._set_success = True
84
83
 
85
84
  # end_position of a fly move, with run_up_distance added on.
86
- self._fly_completed_position: Optional[float] = None
85
+ self._fly_completed_position: float | None = None
87
86
 
88
87
  # Set on kickoff(), complete when motor reaches self._fly_completed_position
89
- self._fly_status: Optional[WatchableAsyncStatus] = None
88
+ self._fly_status: WatchableAsyncStatus | None = None
90
89
 
91
90
  # Set during prepare
92
- self._fly_timeout: Optional[CalculatableTimeout] = CalculateTimeout
91
+ self._fly_timeout: CalculatableTimeout | None = CALCULATE_TIMEOUT
93
92
 
94
93
  super().__init__(name=name)
95
94
 
@@ -119,7 +118,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
119
118
  )
120
119
 
121
120
  await self.set(fly_prepared_position)
122
- await self.velocity.set(fly_velocity)
121
+ await self.velocity.set(abs(fly_velocity))
123
122
 
124
123
  @AsyncStatus.wrap
125
124
  async def kickoff(self):
@@ -138,9 +137,8 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
138
137
  return self._fly_status
139
138
 
140
139
  @WatchableAsyncStatus.wrap
141
- async def set(
142
- self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
143
- ):
140
+ async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
141
+ new_position = value
144
142
  self._set_success = True
145
143
  (
146
144
  old_position,
@@ -155,7 +153,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
155
153
  self.velocity.get_value(),
156
154
  self.acceleration_time.get_value(),
157
155
  )
158
- if timeout is CalculateTimeout:
156
+ if timeout is CALCULATE_TIMEOUT:
159
157
  assert velocity > 0, "Motor has zero velocity"
160
158
  timeout = (
161
159
  abs(new_position - old_position) / velocity