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.
- ophyd_async/__init__.py +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +102 -74
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +158 -153
- ophyd_async/core/_device.py +143 -115
- ophyd_async/core/_device_filler.py +82 -9
- ophyd_async/core/_flyer.py +16 -7
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +74 -58
- ophyd_async/core/_settings.py +113 -0
- ophyd_async/core/_signal.py +304 -174
- ophyd_async/core/_signal_backend.py +60 -14
- ophyd_async/core/_soft_signal_backend.py +18 -12
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +54 -17
- ophyd_async/core/_utils.py +101 -52
- ophyd_async/core/_yaml_settings.py +66 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +51 -0
- ophyd_async/epics/adandor/_andor_io.py +34 -0
- ophyd_async/epics/adaravis/__init__.py +8 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -41
- ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +36 -14
- ophyd_async/epics/adcore/_core_detector.py +81 -0
- ophyd_async/epics/adcore/_core_io.py +145 -95
- ophyd_async/epics/adcore/_core_logic.py +179 -88
- ophyd_async/epics/adcore/_core_writer.py +223 -0
- ophyd_async/epics/adcore/_hdf_writer.py +51 -92
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +6 -5
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +3 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +32 -27
- ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +7 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +22 -16
- ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +26 -25
- ophyd_async/epics/advimba/_vimba_controller.py +12 -24
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +66 -30
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +50 -18
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +5 -5
- ophyd_async/epics/demo/__init__.py +11 -49
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +83 -38
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +68 -73
- ophyd_async/epics/testing/_utils.py +19 -44
- ophyd_async/epics/testing/test_records.db +16 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -8
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +12 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
- ophyd_async/fastcs/panda/_table.py +13 -7
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +16 -0
- ophyd_async/plan_stubs/_ensure_connected.py +12 -17
- ophyd_async/plan_stubs/_fly.py +3 -5
- ophyd_async/plan_stubs/_nd_attributes.py +9 -5
- ophyd_async/plan_stubs/_panda.py +14 -0
- ophyd_async/plan_stubs/_settings.py +152 -0
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +29 -0
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +21 -33
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +148 -74
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/demo/_counter.py +2 -0
- ophyd_async/tango/demo/_detector.py +2 -0
- ophyd_async/tango/demo/_mover.py +10 -6
- ophyd_async/tango/demo/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +48 -7
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +200 -96
- ophyd_async/testing/_mock_signal_utils.py +59 -73
- ophyd_async/testing/_one_of_everything.py +146 -0
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/demo/_mover.py +0 -95
- ophyd_async/epics/demo/_sensor.py +0 -37
- ophyd_async/epics/demo/sensor.db +0 -19
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
- ophyd_async/sim/demo/_sim_motor.py +0 -107
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
- ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
- {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
|
|
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[
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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()]}
|