ophyd-async 0.8.0a5__py3-none-any.whl → 0.9.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 (116) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +17 -46
  3. ophyd_async/core/_detector.py +68 -44
  4. ophyd_async/core/_device.py +120 -79
  5. ophyd_async/core/_device_filler.py +17 -8
  6. ophyd_async/core/_flyer.py +2 -2
  7. ophyd_async/core/_protocol.py +0 -28
  8. ophyd_async/core/_readable.py +30 -23
  9. ophyd_async/core/_settings.py +104 -0
  10. ophyd_async/core/_signal.py +164 -151
  11. ophyd_async/core/_signal_backend.py +4 -1
  12. ophyd_async/core/_soft_signal_backend.py +2 -1
  13. ophyd_async/core/_table.py +27 -14
  14. ophyd_async/core/_utils.py +30 -5
  15. ophyd_async/core/_yaml_settings.py +64 -0
  16. ophyd_async/epics/adandor/__init__.py +9 -0
  17. ophyd_async/epics/adandor/_andor.py +45 -0
  18. ophyd_async/epics/adandor/_andor_controller.py +49 -0
  19. ophyd_async/epics/adandor/_andor_io.py +36 -0
  20. ophyd_async/epics/adaravis/__init__.py +3 -1
  21. ophyd_async/epics/adaravis/_aravis.py +23 -37
  22. ophyd_async/epics/adaravis/_aravis_controller.py +21 -30
  23. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  24. ophyd_async/epics/adcore/__init__.py +15 -8
  25. ophyd_async/epics/adcore/_core_detector.py +41 -0
  26. ophyd_async/epics/adcore/_core_io.py +56 -31
  27. ophyd_async/epics/adcore/_core_logic.py +99 -84
  28. ophyd_async/epics/adcore/_core_writer.py +219 -0
  29. ophyd_async/epics/adcore/_hdf_writer.py +33 -59
  30. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  31. ophyd_async/epics/adcore/_single_trigger.py +5 -4
  32. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  33. ophyd_async/epics/adcore/_utils.py +37 -36
  34. ophyd_async/epics/adkinetix/_kinetix.py +29 -24
  35. ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
  36. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  37. ophyd_async/epics/adpilatus/__init__.py +2 -2
  38. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  39. ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
  40. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  41. ophyd_async/epics/adsimdetector/__init__.py +3 -3
  42. ophyd_async/epics/adsimdetector/_sim.py +33 -17
  43. ophyd_async/epics/advimba/_vimba.py +23 -23
  44. ophyd_async/epics/advimba/_vimba_controller.py +21 -35
  45. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  46. ophyd_async/epics/core/_aioca.py +52 -21
  47. ophyd_async/epics/core/_p4p.py +59 -16
  48. ophyd_async/epics/core/_pvi_connector.py +4 -2
  49. ophyd_async/epics/core/_signal.py +9 -2
  50. ophyd_async/epics/core/_util.py +10 -1
  51. ophyd_async/epics/eiger/_eiger_controller.py +10 -5
  52. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  53. ophyd_async/epics/motor.py +26 -15
  54. ophyd_async/epics/sim/_ioc.py +29 -0
  55. ophyd_async/epics/{demo → sim}/_mover.py +12 -6
  56. ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
  57. ophyd_async/epics/testing/__init__.py +24 -0
  58. ophyd_async/epics/testing/_example_ioc.py +91 -0
  59. ophyd_async/epics/testing/_utils.py +50 -0
  60. ophyd_async/epics/testing/test_records.db +174 -0
  61. ophyd_async/epics/testing/test_records_pva.db +177 -0
  62. ophyd_async/fastcs/core.py +2 -2
  63. ophyd_async/fastcs/panda/__init__.py +0 -2
  64. ophyd_async/fastcs/panda/_block.py +9 -9
  65. ophyd_async/fastcs/panda/_control.py +9 -4
  66. ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
  67. ophyd_async/fastcs/panda/_table.py +4 -1
  68. ophyd_async/fastcs/panda/_trigger.py +7 -7
  69. ophyd_async/plan_stubs/__init__.py +14 -0
  70. ophyd_async/plan_stubs/_ensure_connected.py +11 -17
  71. ophyd_async/plan_stubs/_fly.py +2 -2
  72. ophyd_async/plan_stubs/_nd_attributes.py +7 -5
  73. ophyd_async/plan_stubs/_panda.py +13 -0
  74. ophyd_async/plan_stubs/_settings.py +125 -0
  75. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  76. ophyd_async/sim/__init__.py +19 -0
  77. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
  78. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
  79. ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
  80. ophyd_async/tango/__init__.py +0 -43
  81. ophyd_async/tango/{signal → core}/__init__.py +7 -2
  82. ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
  83. ophyd_async/tango/{signal → core}/_signal.py +16 -4
  84. ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
  85. ophyd_async/tango/{signal → core}/_tango_transport.py +13 -15
  86. ophyd_async/tango/{demo → sim}/_counter.py +6 -7
  87. ophyd_async/tango/{demo → sim}/_mover.py +13 -9
  88. ophyd_async/testing/__init__.py +52 -0
  89. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  90. ophyd_async/testing/_assert.py +176 -0
  91. ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
  92. ophyd_async/testing/_one_of_everything.py +126 -0
  93. ophyd_async/testing/_wait_for_pending.py +22 -0
  94. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +50 -48
  95. ophyd_async-0.9.0.dist-info/RECORD +129 -0
  96. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/WHEEL +1 -1
  97. ophyd_async/core/_device_save_loader.py +0 -274
  98. ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
  99. ophyd_async/fastcs/panda/_utils.py +0 -16
  100. ophyd_async/sim/demo/__init__.py +0 -19
  101. ophyd_async/sim/testing/__init__.py +0 -0
  102. ophyd_async/tango/base_devices/__init__.py +0 -4
  103. ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
  104. ophyd_async-0.8.0a5.dist-info/entry_points.txt +0 -2
  105. /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
  106. /ophyd_async/epics/{demo → sim}/mover.db +0 -0
  107. /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
  108. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
  109. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
  110. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
  111. /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
  112. /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
  113. /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
  114. /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
  115. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
  116. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,19 @@
1
+ import asyncio
2
+
1
3
  from ophyd_async.core import Device, StrictEnum
4
+ from ophyd_async.core._providers import DatasetDescriber
2
5
  from ophyd_async.epics.core import (
3
6
  epics_signal_r,
4
7
  epics_signal_rw,
5
8
  epics_signal_rw_rbv,
6
9
  )
7
10
 
8
- from ._utils import ADBaseDataType, FileWriteMode, ImageMode
11
+ from ._utils import ADBaseDataType, FileWriteMode, ImageMode, convert_ad_dtype_to_np
9
12
 
10
13
 
11
14
  class Callback(StrictEnum):
12
- Enable = "Enable"
13
- Disable = "Disable"
15
+ ENABLE = "Enable"
16
+ DISABLE = "Disable"
14
17
 
15
18
 
16
19
  class NDArrayBaseIO(Device):
@@ -27,6 +30,21 @@ class NDArrayBaseIO(Device):
27
30
  super().__init__(name=name)
28
31
 
29
32
 
33
+ class ADBaseDatasetDescriber(DatasetDescriber):
34
+ def __init__(self, driver: NDArrayBaseIO) -> None:
35
+ self._driver = driver
36
+
37
+ async def np_datatype(self) -> str:
38
+ return convert_ad_dtype_to_np(await self._driver.data_type.get_value())
39
+
40
+ async def shape(self) -> tuple[int, int]:
41
+ shape = await asyncio.gather(
42
+ self._driver.array_size_y.get_value(),
43
+ self._driver.array_size_x.get_value(),
44
+ )
45
+ return shape
46
+
47
+
30
48
  class NDPluginBaseIO(NDArrayBaseIO):
31
49
  def __init__(self, prefix: str, name: str = "") -> None:
32
50
  self.nd_array_port = epics_signal_rw_rbv(str, prefix + "NDArrayPort")
@@ -36,6 +54,7 @@ class NDPluginBaseIO(NDArrayBaseIO):
36
54
  self.nd_array_address = epics_signal_rw_rbv(int, prefix + "NDArrayAddress")
37
55
  self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV")
38
56
  self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV")
57
+ self.queue_size = epics_signal_rw(int, prefix + "QueueSize")
39
58
  super().__init__(prefix, name)
40
59
 
41
60
 
@@ -72,17 +91,17 @@ class DetectorState(StrictEnum):
72
91
  See definition in ADApp/ADSrc/ADDriver.h in https://github.com/areaDetector/ADCore
73
92
  """
74
93
 
75
- Idle = "Idle"
76
- Acquire = "Acquire"
77
- Readout = "Readout"
78
- Correct = "Correct"
79
- Saving = "Saving"
80
- Aborting = "Aborting"
81
- Error = "Error"
82
- Waiting = "Waiting"
83
- Initializing = "Initializing"
84
- Disconnected = "Disconnected"
85
- Aborted = "Aborted"
94
+ IDLE = "Idle"
95
+ ACQUIRE = "Acquire"
96
+ READOUT = "Readout"
97
+ CORRECT = "Correct"
98
+ SAVING = "Saving"
99
+ ABORTING = "Aborting"
100
+ ERROR = "Error"
101
+ WAITING = "Waiting"
102
+ INITIALIZING = "Initializing"
103
+ DISCONNECTED = "Disconnected"
104
+ ABORTED = "Aborted"
86
105
 
87
106
 
88
107
  class ADBaseIO(NDArrayBaseIO):
@@ -99,40 +118,46 @@ class ADBaseIO(NDArrayBaseIO):
99
118
 
100
119
 
101
120
  class Compression(StrictEnum):
102
- none = "None"
103
- nbit = "N-bit"
104
- szip = "szip"
105
- zlib = "zlib"
106
- blosc = "Blosc"
107
- bslz4 = "BSLZ4"
108
- lz4 = "LZ4"
109
- jpeg = "JPEG"
121
+ NONE = "None"
122
+ NBIT = "N-bit"
123
+ SZIP = "szip"
124
+ ZLIB = "zlib"
125
+ BLOSC = "Blosc"
126
+ BSLZ4 = "BSLZ4"
127
+ LZ4 = "LZ4"
128
+ JPEG = "JPEG"
110
129
 
111
130
 
112
- class NDFileHDFIO(NDPluginBaseIO):
131
+ class NDFileIO(NDPluginBaseIO):
113
132
  def __init__(self, prefix: str, name="") -> None:
114
- # Define some signals
115
- self.position_mode = epics_signal_rw_rbv(bool, prefix + "PositionMode")
116
- self.compression = epics_signal_rw_rbv(Compression, prefix + "Compression")
117
- self.num_extra_dims = epics_signal_rw_rbv(int, prefix + "NumExtraDims")
118
133
  self.file_path = epics_signal_rw_rbv(str, prefix + "FilePath")
119
134
  self.file_name = epics_signal_rw_rbv(str, prefix + "FileName")
120
135
  self.file_path_exists = epics_signal_r(bool, prefix + "FilePathExists_RBV")
121
136
  self.file_template = epics_signal_rw_rbv(str, prefix + "FileTemplate")
122
137
  self.full_file_name = epics_signal_r(str, prefix + "FullFileName_RBV")
138
+ self.file_number = epics_signal_rw(int, prefix + "FileNumber")
139
+ self.auto_increment = epics_signal_rw(bool, prefix + "AutoIncrement")
123
140
  self.file_write_mode = epics_signal_rw_rbv(
124
141
  FileWriteMode, prefix + "FileWriteMode"
125
142
  )
126
143
  self.num_capture = epics_signal_rw_rbv(int, prefix + "NumCapture")
127
144
  self.num_captured = epics_signal_r(int, prefix + "NumCaptured_RBV")
128
- self.swmr_mode = epics_signal_rw_rbv(bool, prefix + "SWMRMode")
129
- self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen")
130
145
  self.capture = epics_signal_rw_rbv(bool, prefix + "Capture")
131
- self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
132
- self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName")
133
146
  self.array_size0 = epics_signal_r(int, prefix + "ArraySize0")
134
147
  self.array_size1 = epics_signal_r(int, prefix + "ArraySize1")
135
148
  self.create_directory = epics_signal_rw(int, prefix + "CreateDirectory")
149
+ super().__init__(prefix, name)
150
+
151
+
152
+ class NDFileHDFIO(NDFileIO):
153
+ def __init__(self, prefix: str, name="") -> None:
154
+ self.position_mode = epics_signal_rw_rbv(bool, prefix + "PositionMode")
155
+ self.compression = epics_signal_rw_rbv(Compression, prefix + "Compression")
156
+ self.num_extra_dims = epics_signal_rw_rbv(int, prefix + "NumExtraDims")
157
+ self.swmr_mode = epics_signal_rw_rbv(bool, prefix + "SWMRMode")
158
+ self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
159
+ self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName")
136
160
  self.num_frames_chunks = epics_signal_r(int, prefix + "NumFramesChunks_RBV")
137
161
  self.chunk_size_auto = epics_signal_rw_rbv(bool, prefix + "ChunkSizeAuto")
162
+ self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen")
138
163
  super().__init__(prefix, name)
@@ -1,106 +1,121 @@
1
1
  import asyncio
2
+ from typing import Generic, TypeVar
2
3
 
3
4
  from ophyd_async.core import (
4
5
  DEFAULT_TIMEOUT,
5
6
  AsyncStatus,
6
- DatasetDescriber,
7
7
  DetectorController,
8
+ DetectorTrigger,
9
+ TriggerInfo,
8
10
  set_and_wait_for_value,
9
11
  )
10
- from ophyd_async.epics.adcore._utils import convert_ad_dtype_to_np
11
12
 
12
13
  from ._core_io import ADBaseIO, DetectorState
14
+ from ._utils import ImageMode, stop_busy_record
13
15
 
14
16
  # Default set of states that we should consider "good" i.e. the acquisition
15
17
  # is complete and went well
16
18
  DEFAULT_GOOD_STATES: frozenset[DetectorState] = frozenset(
17
- [DetectorState.Idle, DetectorState.Aborted]
19
+ [DetectorState.IDLE, DetectorState.ABORTED]
18
20
  )
19
21
 
20
-
21
- class ADBaseDatasetDescriber(DatasetDescriber):
22
- def __init__(self, driver: ADBaseIO) -> None:
23
- self._driver = driver
24
-
25
- async def np_datatype(self) -> str:
26
- return convert_ad_dtype_to_np(await self._driver.data_type.get_value())
27
-
28
- async def shape(self) -> tuple[int, int]:
29
- shape = await asyncio.gather(
30
- self._driver.array_size_y.get_value(),
31
- self._driver.array_size_x.get_value(),
22
+ ADBaseIOT = TypeVar("ADBaseIOT", bound=ADBaseIO)
23
+ ADBaseControllerT = TypeVar("ADBaseControllerT", bound="ADBaseController")
24
+
25
+
26
+ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
27
+ def __init__(
28
+ self,
29
+ driver: ADBaseIOT,
30
+ good_states: frozenset[DetectorState] = DEFAULT_GOOD_STATES,
31
+ ) -> None:
32
+ self.driver = driver
33
+ self.good_states = good_states
34
+ self.frame_timeout = DEFAULT_TIMEOUT
35
+ self._arm_status: AsyncStatus | None = None
36
+
37
+ async def prepare(self, trigger_info: TriggerInfo) -> None:
38
+ if trigger_info.trigger != DetectorTrigger.INTERNAL:
39
+ msg = (
40
+ "fly scanning (i.e. external triggering) is not supported for this "
41
+ "device"
42
+ )
43
+ raise TypeError(msg)
44
+ self.frame_timeout = (
45
+ DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
32
46
  )
33
- return shape
34
-
35
-
36
- async def set_exposure_time_and_acquire_period_if_supplied(
37
- controller: DetectorController,
38
- driver: ADBaseIO,
39
- exposure: float | None = None,
40
- timeout: float = DEFAULT_TIMEOUT,
41
- ) -> None:
42
- """
43
- Sets the exposure time if it is not None and the acquire period to the
44
- exposure time plus the deadtime. This is expected behavior for most
45
- AreaDetectors, but some may require more specialized handling.
46
-
47
- Parameters
48
- ----------
49
- controller:
50
- Controller that can supply a deadtime.
51
- driver:
52
- The driver to start acquiring. Must subclass ADBaseIO.
53
- exposure:
54
- Desired exposure time, this is a noop if it is None.
55
- timeout:
56
- How long to wait for the exposure time and acquire period to be set.
57
- """
58
- if exposure is not None:
59
- full_frame_time = exposure + controller.get_deadtime(exposure)
60
47
  await asyncio.gather(
61
- driver.acquire_time.set(exposure, timeout=timeout),
62
- driver.acquire_period.set(full_frame_time, timeout=timeout),
48
+ self.driver.num_images.set(trigger_info.total_number_of_triggers),
49
+ self.driver.image_mode.set(ImageMode.MULTIPLE),
63
50
  )
64
51
 
65
-
66
- async def start_acquiring_driver_and_ensure_status(
67
- driver: ADBaseIO,
68
- good_states: frozenset[DetectorState] = frozenset(DEFAULT_GOOD_STATES),
69
- timeout: float = DEFAULT_TIMEOUT,
70
- ) -> AsyncStatus:
71
- """
72
- Start acquiring driver, raising ValueError if the detector is in a bad state.
73
-
74
- This sets driver.acquire to True, and waits for it to be True up to a timeout.
75
- Then, it checks that the DetectorState PV is in DEFAULT_GOOD_STATES, and otherwise
76
- raises a ValueError.
77
-
78
- Parameters
79
- ----------
80
- driver:
81
- The driver to start acquiring. Must subclass ADBaseIO.
82
- good_states:
83
- set of states defined in DetectorState enum which are considered good states.
84
- timeout:
85
- How long to wait for driver.acquire to readback True (i.e. acquiring).
86
-
87
- Returns
88
- -------
89
- AsyncStatus:
90
- An AsyncStatus that can be awaited to set driver.acquire to True and perform
91
- subsequent raising (if applicable) due to detector state.
92
- """
93
-
94
- status = await set_and_wait_for_value(driver.acquire, True, timeout=timeout)
95
-
96
- async def complete_acquisition() -> None:
97
- """NOTE: possible race condition here between the callback from
98
- set_and_wait_for_value and the detector state updating."""
99
- await status
100
- state = await driver.detector_state.get_value()
101
- if state not in good_states:
102
- raise ValueError(
103
- f"Final detector state {state} not in valid end states: {good_states}"
52
+ async def arm(self):
53
+ self._arm_status = await self.start_acquiring_driver_and_ensure_status()
54
+
55
+ async def wait_for_idle(self):
56
+ if self._arm_status:
57
+ await self._arm_status
58
+
59
+ async def disarm(self):
60
+ # We can't use caput callback as we already used it in arm() and we can't have
61
+ # 2 or they will deadlock
62
+ await stop_busy_record(self.driver.acquire, False, timeout=1)
63
+
64
+ async def set_exposure_time_and_acquire_period_if_supplied(
65
+ self,
66
+ exposure: float | None = None,
67
+ timeout: float = DEFAULT_TIMEOUT,
68
+ ) -> None:
69
+ """
70
+ Sets the exposure time if it is not None and the acquire period to the
71
+ exposure time plus the deadtime. This is expected behavior for most
72
+ AreaDetectors, but some may require more specialized handling.
73
+
74
+ Parameters
75
+ ----------
76
+ exposure:
77
+ Desired exposure time, this is a noop if it is None.
78
+ timeout:
79
+ How long to wait for the exposure time and acquire period to be set.
80
+ """
81
+ if exposure is not None:
82
+ full_frame_time = exposure + self.get_deadtime(exposure)
83
+ await asyncio.gather(
84
+ self.driver.acquire_time.set(exposure, timeout=timeout),
85
+ self.driver.acquire_period.set(full_frame_time, timeout=timeout),
104
86
  )
105
87
 
106
- return AsyncStatus(complete_acquisition())
88
+ async def start_acquiring_driver_and_ensure_status(self) -> AsyncStatus:
89
+ """
90
+ Start acquiring driver, raising ValueError if the detector is in a bad state.
91
+
92
+ This sets driver.acquire to True, and waits for it to be True up to a timeout.
93
+ Then, it checks that the DetectorState PV is in DEFAULT_GOOD_STATES,
94
+ and otherwise raises a ValueError.
95
+
96
+ Returns
97
+ -------
98
+ AsyncStatus:
99
+ An AsyncStatus that can be awaited to set driver.acquire to True and perform
100
+ subsequent raising (if applicable) due to detector state.
101
+ """
102
+
103
+ status = await set_and_wait_for_value(
104
+ self.driver.acquire,
105
+ True,
106
+ timeout=DEFAULT_TIMEOUT,
107
+ wait_for_set_completion=False,
108
+ )
109
+
110
+ async def complete_acquisition() -> None:
111
+ """NOTE: possible race condition here between the callback from
112
+ set_and_wait_for_value and the detector state updating."""
113
+ await status
114
+ state = await self.driver.detector_state.get_value()
115
+ if state not in self.good_states:
116
+ raise ValueError(
117
+ f"Final detector state {state.value} not "
118
+ "in valid end states: {self.good_states}"
119
+ )
120
+
121
+ return AsyncStatus(complete_acquisition())
@@ -0,0 +1,219 @@
1
+ import asyncio
2
+ from collections.abc import AsyncGenerator, AsyncIterator
3
+ from pathlib import Path
4
+ from typing import Generic, TypeVar, get_args
5
+ from urllib.parse import urlunparse
6
+
7
+ from bluesky.protocols import Hints, StreamAsset
8
+ from event_model import (
9
+ ComposeStreamResource,
10
+ DataKey,
11
+ StreamRange,
12
+ )
13
+
14
+ from ophyd_async.core._detector import DetectorWriter
15
+ from ophyd_async.core._providers import DatasetDescriber, NameProvider, PathProvider
16
+ from ophyd_async.core._signal import (
17
+ observe_value,
18
+ set_and_wait_for_value,
19
+ wait_for_value,
20
+ )
21
+ from ophyd_async.core._status import AsyncStatus
22
+ from ophyd_async.core._utils import DEFAULT_TIMEOUT
23
+
24
+ # from ophyd_async.epics.adcore._core_logic import ADBaseDatasetDescriber
25
+ from ._core_io import (
26
+ ADBaseDatasetDescriber,
27
+ Callback,
28
+ NDArrayBaseIO,
29
+ NDFileIO,
30
+ NDPluginBaseIO,
31
+ )
32
+ from ._utils import FileWriteMode
33
+
34
+ NDFileIOT = TypeVar("NDFileIOT", bound=NDFileIO)
35
+ ADWriterT = TypeVar("ADWriterT", bound="ADWriter")
36
+
37
+
38
+ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
39
+ default_suffix: str = "FILE1:"
40
+
41
+ def __init__(
42
+ self,
43
+ fileio: NDFileIOT,
44
+ path_provider: PathProvider,
45
+ name_provider: NameProvider,
46
+ dataset_describer: DatasetDescriber,
47
+ file_extension: str = "",
48
+ mimetype: str = "",
49
+ plugins: dict[str, NDPluginBaseIO] | None = None,
50
+ ) -> None:
51
+ self._plugins = plugins or {}
52
+ self.fileio = fileio
53
+ self._path_provider = path_provider
54
+ self._name_provider = name_provider
55
+ self._dataset_describer = dataset_describer
56
+ self._file_extension = file_extension
57
+ self._mimetype = mimetype
58
+ self._last_emitted = 0
59
+ self._emitted_resource = None
60
+
61
+ self._capture_status: AsyncStatus | None = None
62
+ self._multiplier = 1
63
+ self._filename_template = "%s%s_%6.6d"
64
+
65
+ @classmethod
66
+ def with_io(
67
+ cls: type[ADWriterT],
68
+ prefix: str,
69
+ path_provider: PathProvider,
70
+ dataset_source: NDArrayBaseIO | None = None,
71
+ fileio_suffix: str | None = None,
72
+ plugins: dict[str, NDPluginBaseIO] | None = None,
73
+ ) -> ADWriterT:
74
+ try:
75
+ fileio_cls = get_args(cls.__orig_bases__[0])[0] # type: ignore
76
+ except IndexError as err:
77
+ raise RuntimeError("File IO class for writer not specified!") from err
78
+
79
+ fileio = fileio_cls(prefix + (fileio_suffix or cls.default_suffix))
80
+ dataset_describer = ADBaseDatasetDescriber(dataset_source or fileio)
81
+
82
+ def name_provider() -> str:
83
+ if fileio.parent == "Not attached to a detector":
84
+ raise RuntimeError("Initializing writer without parent detector!")
85
+ return fileio.parent.name
86
+
87
+ writer = cls(
88
+ fileio, path_provider, name_provider, dataset_describer, plugins=plugins
89
+ )
90
+ return writer
91
+
92
+ async def begin_capture(self) -> None:
93
+ info = self._path_provider(device_name=self._name_provider())
94
+
95
+ await self.fileio.enable_callbacks.set(Callback.ENABLE)
96
+
97
+ # Set the directory creation depth first, since dir creation callback happens
98
+ # when directory path PV is processed.
99
+ await self.fileio.create_directory.set(info.create_dir_depth)
100
+
101
+ await asyncio.gather(
102
+ # See https://github.com/bluesky/ophyd-async/issues/122
103
+ self.fileio.file_path.set(str(info.directory_path)),
104
+ self.fileio.file_name.set(info.filename),
105
+ self.fileio.file_write_mode.set(FileWriteMode.STREAM),
106
+ # For non-HDF file writers, use AD file templating mechanism
107
+ # for generating multi-image datasets
108
+ self.fileio.file_template.set(
109
+ self._filename_template + self._file_extension
110
+ ),
111
+ self.fileio.auto_increment.set(True),
112
+ self.fileio.file_number.set(0),
113
+ )
114
+
115
+ if not await self.fileio.file_path_exists.get_value():
116
+ msg = f"File path {info.directory_path} for file plugin does not exist"
117
+ raise FileNotFoundError(msg)
118
+
119
+ # Overwrite num_capture to go forever
120
+ await self.fileio.num_capture.set(0)
121
+ # Wait for it to start, stashing the status that tells us when it finishes
122
+ self._capture_status = await set_and_wait_for_value(
123
+ self.fileio.capture, True, wait_for_set_completion=False
124
+ )
125
+
126
+ async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
127
+ self._emitted_resource = None
128
+ self._last_emitted = 0
129
+ self._multiplier = multiplier
130
+ frame_shape = await self._dataset_describer.shape()
131
+ dtype_numpy = await self._dataset_describer.np_datatype()
132
+
133
+ await self.begin_capture()
134
+
135
+ describe = {
136
+ self._name_provider(): DataKey(
137
+ source=self._name_provider(),
138
+ shape=list(frame_shape),
139
+ dtype="array",
140
+ dtype_numpy=dtype_numpy,
141
+ external="STREAM:",
142
+ ) # type: ignore
143
+ }
144
+ return describe
145
+
146
+ async def observe_indices_written(
147
+ self, timeout=DEFAULT_TIMEOUT
148
+ ) -> AsyncGenerator[int, None]:
149
+ """Wait until a specific index is ready to be collected"""
150
+ async for num_captured in observe_value(self.fileio.num_captured, timeout):
151
+ yield num_captured // self._multiplier
152
+
153
+ async def get_indices_written(self) -> int:
154
+ num_captured = await self.fileio.num_captured.get_value()
155
+ return num_captured // self._multiplier
156
+
157
+ async def collect_stream_docs(
158
+ self, indices_written: int
159
+ ) -> AsyncIterator[StreamAsset]:
160
+ if indices_written:
161
+ if not self._emitted_resource:
162
+ file_path = Path(await self.fileio.file_path.get_value())
163
+ file_name = await self.fileio.file_name.get_value()
164
+ file_template = file_name + "_{:06d}" + self._file_extension
165
+
166
+ frame_shape = await self._dataset_describer.shape()
167
+
168
+ uri = urlunparse(
169
+ (
170
+ "file",
171
+ "localhost",
172
+ str(file_path.absolute()) + "/",
173
+ "",
174
+ "",
175
+ None,
176
+ )
177
+ )
178
+
179
+ bundler_composer = ComposeStreamResource()
180
+
181
+ self._emitted_resource = bundler_composer(
182
+ mimetype=self._mimetype,
183
+ uri=uri,
184
+ data_key=self._name_provider(),
185
+ parameters={
186
+ # Assume that we always write 1 frame per file/chunk
187
+ "chunk_shape": (1, *frame_shape),
188
+ # Include file template for reconstruction in consolidator
189
+ "template": file_template,
190
+ },
191
+ uid=None,
192
+ validate=True,
193
+ )
194
+
195
+ yield "stream_resource", self._emitted_resource.stream_resource_doc
196
+
197
+ # Indices are relative to resource
198
+ if indices_written > self._last_emitted:
199
+ indices: StreamRange = {
200
+ "start": self._last_emitted,
201
+ "stop": indices_written,
202
+ }
203
+ self._last_emitted = indices_written
204
+ yield (
205
+ "stream_datum",
206
+ self._emitted_resource.compose_stream_datum(indices),
207
+ )
208
+
209
+ async def close(self):
210
+ # Already done a caput callback in _capture_status, so can't do one here
211
+ await self.fileio.capture.set(False, wait=False)
212
+ await wait_for_value(self.fileio.capture, False, DEFAULT_TIMEOUT)
213
+ if self._capture_status:
214
+ # We kicked off an open, so wait for it to return
215
+ await self._capture_status
216
+
217
+ @property
218
+ def hints(self) -> Hints:
219
+ return {"fields": [self._name_provider()]}