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.
- ophyd_async/__init__.py +10 -1
- ophyd_async/__main__.py +12 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +15 -6
- ophyd_async/core/_detector.py +72 -63
- ophyd_async/core/_device.py +13 -15
- ophyd_async/core/_device_save_loader.py +30 -19
- ophyd_async/core/_flyer.py +6 -4
- ophyd_async/core/_hdf_dataset.py +8 -9
- ophyd_async/core/_log.py +3 -1
- ophyd_async/core/_mock_signal_backend.py +11 -9
- ophyd_async/core/_mock_signal_utils.py +8 -5
- ophyd_async/core/_protocol.py +7 -7
- ophyd_async/core/_providers.py +17 -13
- ophyd_async/core/_readable.py +30 -22
- ophyd_async/core/_signal.py +53 -52
- ophyd_async/core/_signal_backend.py +20 -7
- ophyd_async/core/_soft_signal_backend.py +62 -32
- ophyd_async/core/_status.py +18 -4
- ophyd_async/core/_table.py +63 -0
- ophyd_async/core/_utils.py +24 -28
- ophyd_async/epics/adaravis/_aravis.py +1 -1
- ophyd_async/epics/adaravis/_aravis_controller.py +17 -16
- ophyd_async/epics/adaravis/_aravis_io.py +2 -1
- ophyd_async/epics/adcore/__init__.py +2 -2
- ophyd_async/epics/adcore/_core_io.py +2 -0
- ophyd_async/epics/adcore/_core_logic.py +9 -7
- ophyd_async/epics/adcore/_hdf_writer.py +26 -21
- ophyd_async/epics/adcore/_single_trigger.py +1 -1
- ophyd_async/epics/adcore/_utils.py +5 -6
- ophyd_async/epics/adkinetix/_kinetix.py +1 -1
- ophyd_async/epics/adkinetix/_kinetix_controller.py +19 -14
- ophyd_async/epics/adpilatus/_pilatus.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_controller.py +18 -16
- ophyd_async/epics/adsimdetector/_sim.py +7 -6
- ophyd_async/epics/adsimdetector/_sim_controller.py +20 -15
- ophyd_async/epics/advimba/_vimba.py +1 -1
- ophyd_async/epics/advimba/_vimba_controller.py +21 -16
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/sensor.db +0 -1
- ophyd_async/epics/eiger/__init__.py +5 -0
- ophyd_async/epics/eiger/_eiger.py +43 -0
- ophyd_async/epics/eiger/_eiger_controller.py +66 -0
- ophyd_async/epics/eiger/_eiger_io.py +42 -0
- ophyd_async/epics/eiger/_odin_io.py +126 -0
- ophyd_async/epics/motor.py +9 -11
- ophyd_async/epics/pvi/_pvi.py +30 -33
- ophyd_async/epics/signal/_aioca.py +55 -25
- ophyd_async/epics/signal/_common.py +3 -10
- ophyd_async/epics/signal/_epics_transport.py +11 -8
- ophyd_async/epics/signal/_p4p.py +79 -30
- ophyd_async/epics/signal/_signal.py +6 -8
- ophyd_async/fastcs/panda/__init__.py +0 -6
- ophyd_async/fastcs/panda/_control.py +14 -15
- ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
- ophyd_async/fastcs/panda/_table.py +111 -138
- ophyd_async/fastcs/panda/_trigger.py +1 -2
- ophyd_async/fastcs/panda/_utils.py +3 -2
- ophyd_async/fastcs/panda/_writer.py +28 -13
- ophyd_async/plan_stubs/_fly.py +16 -16
- ophyd_async/plan_stubs/_nd_attributes.py +12 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +24 -20
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
- ophyd_async/sim/demo/_sim_motor.py +2 -1
- {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/METADATA +46 -45
- ophyd_async-0.6.0.dist-info/RECORD +96 -0
- {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/WHEEL +1 -1
- ophyd_async-0.5.1.dist-info/RECORD +0 -90
- {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/entry_points.txt +0 -0
- {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:
|
|
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
|
|
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 =
|
|
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(
|
|
35
|
+
self.driver.num_images.set(trigger_info.number),
|
|
37
36
|
self.driver.image_mode.set(adcore.ImageMode.multiple),
|
|
38
37
|
)
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import Optional
|
|
3
2
|
|
|
4
|
-
from ophyd_async.core import
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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)
|
ophyd_async/epics/demo/_mover.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
ophyd_async/epics/demo/sensor.db
CHANGED
|
@@ -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)
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -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=
|
|
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:
|
|
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:
|
|
88
|
+
self._fly_status: WatchableAsyncStatus | None = None
|
|
90
89
|
|
|
91
90
|
# Set during prepare
|
|
92
|
-
self._fly_timeout:
|
|
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
|
-
|
|
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
|
|
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
|