ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__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 (157) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +102 -74
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +158 -153
  8. ophyd_async/core/_device.py +143 -115
  9. ophyd_async/core/_device_filler.py +82 -9
  10. ophyd_async/core/_flyer.py +16 -7
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +74 -58
  17. ophyd_async/core/_settings.py +113 -0
  18. ophyd_async/core/_signal.py +304 -174
  19. ophyd_async/core/_signal_backend.py +60 -14
  20. ophyd_async/core/_soft_signal_backend.py +18 -12
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +54 -17
  23. ophyd_async/core/_utils.py +101 -52
  24. ophyd_async/core/_yaml_settings.py +66 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/__init__.py +9 -0
  27. ophyd_async/epics/adandor/_andor.py +45 -0
  28. ophyd_async/epics/adandor/_andor_controller.py +51 -0
  29. ophyd_async/epics/adandor/_andor_io.py +34 -0
  30. ophyd_async/epics/adaravis/__init__.py +8 -1
  31. ophyd_async/epics/adaravis/_aravis.py +23 -41
  32. ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
  33. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  34. ophyd_async/epics/adcore/__init__.py +36 -14
  35. ophyd_async/epics/adcore/_core_detector.py +81 -0
  36. ophyd_async/epics/adcore/_core_io.py +145 -95
  37. ophyd_async/epics/adcore/_core_logic.py +179 -88
  38. ophyd_async/epics/adcore/_core_writer.py +223 -0
  39. ophyd_async/epics/adcore/_hdf_writer.py +51 -92
  40. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  41. ophyd_async/epics/adcore/_single_trigger.py +6 -5
  42. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  43. ophyd_async/epics/adcore/_utils.py +3 -2
  44. ophyd_async/epics/adkinetix/__init__.py +2 -1
  45. ophyd_async/epics/adkinetix/_kinetix.py +32 -27
  46. ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
  47. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  48. ophyd_async/epics/adpilatus/__init__.py +7 -2
  49. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  50. ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
  51. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  52. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  53. ophyd_async/epics/adsimdetector/_sim.py +22 -16
  54. ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
  55. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  56. ophyd_async/epics/advimba/__init__.py +10 -1
  57. ophyd_async/epics/advimba/_vimba.py +26 -25
  58. ophyd_async/epics/advimba/_vimba_controller.py +12 -24
  59. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  60. ophyd_async/epics/core/_aioca.py +66 -30
  61. ophyd_async/epics/core/_epics_connector.py +4 -0
  62. ophyd_async/epics/core/_epics_device.py +2 -0
  63. ophyd_async/epics/core/_p4p.py +50 -18
  64. ophyd_async/epics/core/_pvi_connector.py +65 -8
  65. ophyd_async/epics/core/_signal.py +51 -51
  66. ophyd_async/epics/core/_util.py +5 -5
  67. ophyd_async/epics/demo/__init__.py +11 -49
  68. ophyd_async/epics/demo/__main__.py +31 -0
  69. ophyd_async/epics/demo/_ioc.py +32 -0
  70. ophyd_async/epics/demo/_motor.py +82 -0
  71. ophyd_async/epics/demo/_point_detector.py +42 -0
  72. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  73. ophyd_async/epics/demo/_stage.py +15 -0
  74. ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
  75. ophyd_async/epics/demo/point_detector.db +59 -0
  76. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  77. ophyd_async/epics/eiger/_eiger.py +1 -3
  78. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  79. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  80. ophyd_async/epics/eiger/_odin_io.py +1 -2
  81. ophyd_async/epics/motor.py +83 -38
  82. ophyd_async/epics/signal.py +4 -1
  83. ophyd_async/epics/testing/__init__.py +14 -14
  84. ophyd_async/epics/testing/_example_ioc.py +68 -73
  85. ophyd_async/epics/testing/_utils.py +19 -44
  86. ophyd_async/epics/testing/test_records.db +16 -0
  87. ophyd_async/epics/testing/test_records_pva.db +17 -16
  88. ophyd_async/fastcs/__init__.py +1 -0
  89. ophyd_async/fastcs/core.py +6 -0
  90. ophyd_async/fastcs/odin/__init__.py +1 -0
  91. ophyd_async/fastcs/panda/__init__.py +8 -8
  92. ophyd_async/fastcs/panda/_block.py +29 -9
  93. ophyd_async/fastcs/panda/_control.py +12 -2
  94. ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
  95. ophyd_async/fastcs/panda/_table.py +13 -7
  96. ophyd_async/fastcs/panda/_trigger.py +23 -9
  97. ophyd_async/fastcs/panda/_writer.py +27 -30
  98. ophyd_async/plan_stubs/__init__.py +16 -0
  99. ophyd_async/plan_stubs/_ensure_connected.py +12 -17
  100. ophyd_async/plan_stubs/_fly.py +3 -5
  101. ophyd_async/plan_stubs/_nd_attributes.py +9 -5
  102. ophyd_async/plan_stubs/_panda.py +14 -0
  103. ophyd_async/plan_stubs/_settings.py +152 -0
  104. ophyd_async/plan_stubs/_utils.py +3 -0
  105. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  106. ophyd_async/sim/__init__.py +29 -0
  107. ophyd_async/sim/__main__.py +43 -0
  108. ophyd_async/sim/_blob_detector.py +33 -0
  109. ophyd_async/sim/_blob_detector_controller.py +48 -0
  110. ophyd_async/sim/_blob_detector_writer.py +105 -0
  111. ophyd_async/sim/_mirror_horizontal.py +46 -0
  112. ophyd_async/sim/_mirror_vertical.py +74 -0
  113. ophyd_async/sim/_motor.py +233 -0
  114. ophyd_async/sim/_pattern_generator.py +124 -0
  115. ophyd_async/sim/_point_detector.py +86 -0
  116. ophyd_async/sim/_stage.py +19 -0
  117. ophyd_async/tango/__init__.py +1 -0
  118. ophyd_async/tango/core/__init__.py +6 -1
  119. ophyd_async/tango/core/_base_device.py +41 -33
  120. ophyd_async/tango/core/_converters.py +81 -0
  121. ophyd_async/tango/core/_signal.py +21 -33
  122. ophyd_async/tango/core/_tango_readable.py +2 -19
  123. ophyd_async/tango/core/_tango_transport.py +148 -74
  124. ophyd_async/tango/core/_utils.py +47 -0
  125. ophyd_async/tango/demo/_counter.py +2 -0
  126. ophyd_async/tango/demo/_detector.py +2 -0
  127. ophyd_async/tango/demo/_mover.py +10 -6
  128. ophyd_async/tango/demo/_tango/_servers.py +4 -0
  129. ophyd_async/tango/testing/__init__.py +6 -0
  130. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  131. ophyd_async/testing/__init__.py +48 -7
  132. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  133. ophyd_async/testing/_assert.py +200 -96
  134. ophyd_async/testing/_mock_signal_utils.py +59 -73
  135. ophyd_async/testing/_one_of_everything.py +146 -0
  136. ophyd_async/testing/_single_derived.py +87 -0
  137. ophyd_async/testing/_utils.py +3 -0
  138. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
  139. ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
  140. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
  141. ophyd_async/core/_device_save_loader.py +0 -274
  142. ophyd_async/epics/demo/_mover.py +0 -95
  143. ophyd_async/epics/demo/_sensor.py +0 -37
  144. ophyd_async/epics/demo/sensor.db +0 -19
  145. ophyd_async/fastcs/panda/_utils.py +0 -16
  146. ophyd_async/sim/demo/__init__.py +0 -19
  147. ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
  148. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
  149. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
  150. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
  151. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
  152. ophyd_async/sim/demo/_sim_motor.py +0 -107
  153. ophyd_async/sim/testing/__init__.py +0 -0
  154. ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
  155. ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
  156. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
  157. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
@@ -1,109 +1,200 @@
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
- from ._core_io import ADBaseIO, DetectorState
13
+ from ._core_io import (
14
+ ADBaseIO,
15
+ ADCallbacks,
16
+ ADState,
17
+ NDCBFlushOnSoftTrgMode,
18
+ NDPluginCBIO,
19
+ )
20
+ from ._utils import ADImageMode, stop_busy_record
13
21
 
14
22
  # Default set of states that we should consider "good" i.e. the acquisition
15
23
  # is complete and went well
16
- DEFAULT_GOOD_STATES: frozenset[DetectorState] = frozenset(
17
- [DetectorState.IDLE, DetectorState.ABORTED]
18
- )
24
+ DEFAULT_GOOD_STATES: frozenset[ADState] = frozenset([ADState.IDLE, ADState.ABORTED])
25
+
26
+ ADBaseIOT = TypeVar("ADBaseIOT", bound=ADBaseIO)
27
+ ADBaseControllerT = TypeVar("ADBaseControllerT", bound="ADBaseController")
28
+
29
+
30
+ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
31
+ def __init__(
32
+ self,
33
+ driver: ADBaseIOT,
34
+ good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
35
+ ) -> None:
36
+ self.driver = driver
37
+ self.good_states = good_states
38
+ self.frame_timeout = DEFAULT_TIMEOUT
39
+ self._arm_status: AsyncStatus | None = None
40
+
41
+ async def prepare(self, trigger_info: TriggerInfo) -> None:
42
+ if trigger_info.trigger != DetectorTrigger.INTERNAL:
43
+ msg = (
44
+ "fly scanning (i.e. external triggering) is not supported for this "
45
+ "device"
46
+ )
47
+ raise TypeError(msg)
48
+ self.frame_timeout = (
49
+ DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
50
+ )
51
+ await asyncio.gather(
52
+ self.driver.num_images.set(trigger_info.total_number_of_triggers),
53
+ self.driver.image_mode.set(ADImageMode.MULTIPLE),
54
+ )
19
55
 
56
+ async def arm(self):
57
+ self._arm_status = await self.start_acquiring_driver_and_ensure_status()
58
+
59
+ async def wait_for_idle(self):
60
+ if self._arm_status and not self._arm_status.done:
61
+ await self._arm_status
62
+ self._arm_status = None
63
+
64
+ async def disarm(self):
65
+ # We can't use caput callback as we already used it in arm() and we can't have
66
+ # 2 or they will deadlock
67
+ await stop_busy_record(self.driver.acquire, False, timeout=1)
68
+
69
+ async def set_exposure_time_and_acquire_period_if_supplied(
70
+ self,
71
+ exposure: float | None = None,
72
+ timeout: float = DEFAULT_TIMEOUT,
73
+ ) -> None:
74
+ """Set the exposure time and acquire period.
75
+
76
+ If exposure is not None, this sets the acquire time to the exposure time
77
+ and sets the acquire period to the exposure time plus the deadtime. This
78
+ is expected behavior for most AreaDetectors, but some may require more
79
+ specialized handling.
80
+
81
+ :param exposure: Desired exposure time, this is a noop if it is None.
82
+ :type exposure: How long to wait for the exposure time and acquire
83
+ period to be set.
84
+ """
85
+ if exposure is not None:
86
+ full_frame_time = exposure + self.get_deadtime(exposure)
87
+ await asyncio.gather(
88
+ self.driver.acquire_time.set(exposure, timeout=timeout),
89
+ self.driver.acquire_period.set(full_frame_time, timeout=timeout),
90
+ )
20
91
 
21
- class ADBaseDatasetDescriber(DatasetDescriber):
22
- def __init__(self, driver: ADBaseIO) -> None:
23
- self._driver = driver
92
+ async def start_acquiring_driver_and_ensure_status(self) -> AsyncStatus:
93
+ """Start acquiring driver, raising ValueError if the detector is in a bad state.
94
+
95
+ This sets driver.acquire to True, and waits for it to be True up to a timeout.
96
+ Then, it checks that the DetectorState PV is in DEFAULT_GOOD_STATES,
97
+ and otherwise raises a ValueError.
98
+
99
+ :returns AsyncStatus:
100
+ An AsyncStatus that can be awaited to set driver.acquire to True and perform
101
+ subsequent raising (if applicable) due to detector state.
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
+ )
24
109
 
25
- async def np_datatype(self) -> str:
26
- return convert_ad_dtype_to_np(await self._driver.data_type.get_value())
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())
122
+
123
+
124
+ class ADBaseContAcqController(ADBaseController[ADBaseIO]):
125
+ """Continuous acquisition interface for an AreaDetector."""
126
+
127
+ def __init__(self, driver: ADBaseIO, cb_plugin: NDPluginCBIO) -> None:
128
+ self.cb_plugin = cb_plugin
129
+ super().__init__(driver)
130
+
131
+ def get_deadtime(self, exposure):
132
+ # For now just set this to something until we can decide how to pass this in
133
+ return 0.001
134
+
135
+ async def ensure_acquisition_settings_valid(
136
+ self, trigger_info: TriggerInfo
137
+ ) -> None:
138
+ """Ensure the trigger mode is valid for the detector."""
139
+ if trigger_info.trigger != DetectorTrigger.INTERNAL:
140
+ # Note that not all detectors will use the `DetectorTrigger` enum
141
+ raise TypeError(
142
+ "The continuous acq interface only supports internal triggering."
143
+ )
27
144
 
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(),
32
- )
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)
145
+ # Not all detectors allow for changing exposure times during an acquisition,
146
+ # so in this least-common-denominator implementation check to see if
147
+ # exposure time matches the current exposure time.
148
+ exposure_time = await self.driver.acquire_time.get_value()
149
+ if trigger_info.livetime is not None and trigger_info.livetime != exposure_time:
150
+ raise ValueError(
151
+ f"Detector exposure time currently set to {exposure_time}, "
152
+ f"but requested exposure is {trigger_info.livetime}"
153
+ )
154
+
155
+ async def ensure_in_continuous_acquisition_mode(self) -> None:
156
+ """Ensure the detector is in continuous acquisition mode."""
157
+ image_mode = await self.driver.image_mode.get_value()
158
+ acquiring = await self.driver.acquire.get_value()
159
+
160
+ if image_mode != ADImageMode.CONTINUOUS or not acquiring:
161
+ raise RuntimeError(
162
+ "Driver must be acquiring in continuous mode to use the "
163
+ "cont acq interface"
164
+ )
165
+
166
+ async def prepare(self, trigger_info: TriggerInfo) -> None:
167
+ # These are broken out into seperate functions to make it easier to
168
+ # override them in subclasses, for example if you want `prepare` to
169
+ # setup the detector in continuous mode if it isn't already (for now,
170
+ # we assume it already is). If your detector uses different enums
171
+ # for `ImageMode` or `DetectorTrigger`, you should also override these.
172
+ await self.ensure_acquisition_settings_valid(trigger_info)
173
+ await self.ensure_in_continuous_acquisition_mode()
174
+
175
+ # Configure the CB plugin to collect the correct number of triggers
60
176
  await asyncio.gather(
61
- driver.acquire_time.set(exposure, timeout=timeout),
62
- driver.acquire_period.set(full_frame_time, timeout=timeout),
177
+ self.cb_plugin.enable_callbacks.set(ADCallbacks.ENABLE),
178
+ self.cb_plugin.pre_count.set(0),
179
+ self.cb_plugin.post_count.set(trigger_info.total_number_of_triggers),
180
+ self.cb_plugin.preset_trigger_count.set(1),
181
+ self.cb_plugin.flush_on_soft_trg.set(NDCBFlushOnSoftTrgMode.ON_NEW_IMAGE),
63
182
  )
64
183
 
184
+ async def arm(self) -> None:
185
+ # Start the CB plugin's capture, and cache it in the arm status attr
186
+ self._arm_status = await set_and_wait_for_value(
187
+ self.cb_plugin.capture,
188
+ True,
189
+ timeout=DEFAULT_TIMEOUT,
190
+ wait_for_set_completion=False,
191
+ )
65
192
 
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(
95
- driver.acquire, True, timeout=timeout, wait_for_set_completion=False
96
- )
97
-
98
- async def complete_acquisition() -> None:
99
- """NOTE: possible race condition here between the callback from
100
- set_and_wait_for_value and the detector state updating."""
101
- await status
102
- state = await driver.detector_state.get_value()
103
- if state not in good_states:
104
- raise ValueError(
105
- f"Final detector state {state.value} not in valid end "
106
- f"states: {good_states}"
107
- )
193
+ # Send the trigger to begin acquisition
194
+ await self.cb_plugin.trigger.set(True, wait=False)
108
195
 
109
- return AsyncStatus(complete_acquisition())
196
+ async def disarm(self) -> None:
197
+ await stop_busy_record(self.cb_plugin.capture, False, timeout=1)
198
+ if self._arm_status and not self._arm_status.done:
199
+ await self._arm_status
200
+ self._arm_status = None
@@ -0,0 +1,223 @@
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
+ ADCallbacks,
28
+ NDArrayBaseIO,
29
+ NDFileIO,
30
+ NDPluginBaseIO,
31
+ )
32
+ from ._utils import ADFileWriteMode
33
+
34
+ NDFileIOT = TypeVar("NDFileIOT", bound=NDFileIO)
35
+ ADWriterT = TypeVar("ADWriterT", bound="ADWriter")
36
+
37
+
38
+ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
39
+ """Common behavior for all areaDetector writers."""
40
+
41
+ default_suffix: str = "FILE1:"
42
+
43
+ def __init__(
44
+ self,
45
+ fileio: NDFileIOT,
46
+ path_provider: PathProvider,
47
+ name_provider: NameProvider,
48
+ dataset_describer: DatasetDescriber,
49
+ file_extension: str = "",
50
+ mimetype: str = "",
51
+ plugins: dict[str, NDPluginBaseIO] | None = None,
52
+ ) -> None:
53
+ self._plugins = plugins or {}
54
+ self.fileio = fileio
55
+ self._path_provider = path_provider
56
+ self._name_provider = name_provider
57
+ self._dataset_describer = dataset_describer
58
+ self._file_extension = file_extension
59
+ self._mimetype = mimetype
60
+ self._last_emitted = 0
61
+ self._emitted_resource = None
62
+
63
+ self._capture_status: AsyncStatus | None = None
64
+ self._multiplier = 1
65
+ self._filename_template = "%s%s_%6.6d"
66
+
67
+ @classmethod
68
+ def with_io(
69
+ cls: type[ADWriterT],
70
+ prefix: str,
71
+ path_provider: PathProvider,
72
+ dataset_source: NDArrayBaseIO | None = None,
73
+ fileio_suffix: str | None = None,
74
+ plugins: dict[str, NDPluginBaseIO] | None = None,
75
+ ) -> ADWriterT:
76
+ try:
77
+ fileio_cls = get_args(cls.__orig_bases__[0])[0] # type: ignore
78
+ except IndexError as err:
79
+ raise RuntimeError("File IO class for writer not specified!") from err
80
+
81
+ fileio = fileio_cls(prefix + (fileio_suffix or cls.default_suffix))
82
+ dataset_describer = ADBaseDatasetDescriber(dataset_source or fileio)
83
+
84
+ def name_provider() -> str:
85
+ if fileio.parent == "Not attached to a detector":
86
+ raise RuntimeError("Initializing writer without parent detector!")
87
+ return fileio.parent.name
88
+
89
+ writer = cls(
90
+ fileio, path_provider, name_provider, dataset_describer, plugins=plugins
91
+ )
92
+ return writer
93
+
94
+ async def begin_capture(self) -> None:
95
+ info = self._path_provider(device_name=self._name_provider())
96
+
97
+ await self.fileio.enable_callbacks.set(ADCallbacks.ENABLE)
98
+
99
+ # Set the directory creation depth first, since dir creation callback happens
100
+ # when directory path PV is processed.
101
+ await self.fileio.create_directory.set(info.create_dir_depth)
102
+
103
+ await asyncio.gather(
104
+ # See https://github.com/bluesky/ophyd-async/issues/122
105
+ self.fileio.file_path.set(str(info.directory_path)),
106
+ self.fileio.file_name.set(info.filename),
107
+ self.fileio.file_write_mode.set(ADFileWriteMode.STREAM),
108
+ # For non-HDF file writers, use AD file templating mechanism
109
+ # for generating multi-image datasets
110
+ self.fileio.file_template.set(
111
+ self._filename_template + self._file_extension
112
+ ),
113
+ self.fileio.auto_increment.set(True),
114
+ self.fileio.file_number.set(0),
115
+ )
116
+
117
+ if not await self.fileio.file_path_exists.get_value():
118
+ msg = f"File path {info.directory_path} for file plugin does not exist"
119
+ raise FileNotFoundError(msg)
120
+
121
+ # Overwrite num_capture to go forever
122
+ await self.fileio.num_capture.set(0)
123
+ # Wait for it to start, stashing the status that tells us when it finishes
124
+ self._capture_status = await set_and_wait_for_value(
125
+ self.fileio.capture, True, wait_for_set_completion=False
126
+ )
127
+
128
+ async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
129
+ self._emitted_resource = None
130
+ self._last_emitted = 0
131
+ self._multiplier = multiplier
132
+ frame_shape = await self._dataset_describer.shape()
133
+ dtype_numpy = await self._dataset_describer.np_datatype()
134
+
135
+ await self.begin_capture()
136
+
137
+ describe = {
138
+ self._name_provider(): DataKey(
139
+ source=self.fileio.full_file_name.source,
140
+ shape=list(frame_shape),
141
+ dtype="array",
142
+ dtype_numpy=dtype_numpy,
143
+ external="STREAM:",
144
+ ) # type: ignore
145
+ }
146
+ return describe
147
+
148
+ async def observe_indices_written(
149
+ self, timeout: float
150
+ ) -> AsyncGenerator[int, None]:
151
+ """Wait until a specific index is ready to be collected."""
152
+ async for num_captured in observe_value(self.fileio.num_captured, timeout):
153
+ yield num_captured // self._multiplier
154
+
155
+ async def get_indices_written(self) -> int:
156
+ num_captured = await self.fileio.num_captured.get_value()
157
+ return num_captured // self._multiplier
158
+
159
+ async def collect_stream_docs(
160
+ self, indices_written: int
161
+ ) -> AsyncIterator[StreamAsset]:
162
+ if indices_written:
163
+ if not self._emitted_resource:
164
+ file_path = Path(await self.fileio.file_path.get_value())
165
+ file_name = await self.fileio.file_name.get_value()
166
+ file_template = file_name + "_{:06d}" + self._file_extension
167
+
168
+ frame_shape = await self._dataset_describer.shape()
169
+
170
+ uri = urlunparse(
171
+ (
172
+ "file",
173
+ "localhost",
174
+ str(file_path.absolute()) + "/",
175
+ "",
176
+ "",
177
+ None,
178
+ )
179
+ )
180
+
181
+ bundler_composer = ComposeStreamResource()
182
+
183
+ self._emitted_resource = bundler_composer(
184
+ mimetype=self._mimetype,
185
+ uri=uri,
186
+ data_key=self._name_provider(),
187
+ parameters={
188
+ # Assume that we always write 1 frame per file/chunk
189
+ "chunk_shape": (1, *frame_shape),
190
+ # Include file template for reconstruction in consolidator
191
+ "template": file_template,
192
+ "multiplier": self._multiplier,
193
+ },
194
+ uid=None,
195
+ validate=True,
196
+ )
197
+
198
+ yield "stream_resource", self._emitted_resource.stream_resource_doc
199
+
200
+ # Indices are relative to resource
201
+ if indices_written > self._last_emitted:
202
+ indices: StreamRange = {
203
+ "start": self._last_emitted,
204
+ "stop": indices_written,
205
+ }
206
+ self._last_emitted = indices_written
207
+ yield (
208
+ "stream_datum",
209
+ self._emitted_resource.compose_stream_datum(indices),
210
+ )
211
+
212
+ async def close(self):
213
+ # Already done a caput callback in _capture_status, so can't do one here
214
+ await self.fileio.capture.set(False, wait=False)
215
+ await wait_for_value(self.fileio.capture, False, DEFAULT_TIMEOUT)
216
+ if self._capture_status and not self._capture_status.done:
217
+ # We kicked off an open, so wait for it to return
218
+ await self._capture_status
219
+ self._capture_status = None
220
+
221
+ @property
222
+ def hints(self) -> Hints:
223
+ return {"fields": [self._name_provider()]}