dls-dodal 1.29.4__py3-none-any.whl → 1.31.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.
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/METADATA +29 -44
- dls_dodal-1.31.0.dist-info/RECORD +134 -0
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/WHEEL +1 -1
- dls_dodal-1.31.0.dist-info/entry_points.txt +3 -0
- dodal/__init__.py +1 -4
- dodal/_version.py +2 -2
- dodal/beamline_specific_utils/i03.py +1 -4
- dodal/beamlines/__init__.py +7 -1
- dodal/beamlines/i03.py +34 -29
- dodal/beamlines/i04.py +39 -16
- dodal/beamlines/i13_1.py +66 -0
- dodal/beamlines/i22.py +22 -22
- dodal/beamlines/i24.py +1 -1
- dodal/beamlines/p38.py +21 -21
- dodal/beamlines/p45.py +18 -16
- dodal/beamlines/p99.py +61 -0
- dodal/beamlines/training_rig.py +64 -0
- dodal/cli.py +6 -3
- dodal/common/beamlines/beamline_parameters.py +7 -6
- dodal/common/beamlines/beamline_utils.py +15 -14
- dodal/common/maths.py +1 -3
- dodal/common/types.py +6 -5
- dodal/common/udc_directory_provider.py +39 -21
- dodal/common/visit.py +60 -62
- dodal/devices/CTAB.py +22 -17
- dodal/devices/aperture.py +1 -1
- dodal/devices/aperturescatterguard.py +139 -209
- dodal/devices/areadetector/adaravis.py +8 -6
- dodal/devices/areadetector/adsim.py +2 -3
- dodal/devices/areadetector/adutils.py +20 -12
- dodal/devices/areadetector/plugins/MJPG.py +2 -1
- dodal/devices/backlight.py +12 -1
- dodal/devices/cryostream.py +19 -7
- dodal/devices/dcm.py +1 -1
- dodal/devices/detector/__init__.py +13 -2
- dodal/devices/detector/det_dim_constants.py +2 -2
- dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
- dodal/devices/detector/detector.py +33 -32
- dodal/devices/detector/detector_motion.py +38 -31
- dodal/devices/eiger.py +11 -15
- dodal/devices/eiger_odin.py +9 -10
- dodal/devices/fast_grid_scan.py +18 -27
- dodal/devices/fluorescence_detector_motion.py +13 -4
- dodal/devices/focusing_mirror.py +6 -6
- dodal/devices/hutch_shutter.py +4 -4
- dodal/devices/i22/dcm.py +5 -4
- dodal/devices/i22/fswitch.py +10 -6
- dodal/devices/i22/nxsas.py +55 -43
- dodal/devices/i24/aperture.py +1 -1
- dodal/devices/i24/beamstop.py +1 -1
- dodal/devices/i24/dcm.py +1 -1
- dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +1 -1
- dodal/devices/i24/pmac.py +67 -12
- dodal/devices/ipin.py +7 -4
- dodal/devices/linkam3.py +12 -6
- dodal/devices/logging_ophyd_device.py +1 -1
- dodal/devices/motors.py +32 -6
- dodal/devices/oav/grid_overlay.py +1 -0
- dodal/devices/oav/microns_for_zoom_levels.json +1 -1
- dodal/devices/oav/oav_detector.py +2 -1
- dodal/devices/oav/oav_parameters.py +18 -10
- dodal/devices/oav/oav_to_redis_forwarder.py +129 -0
- dodal/devices/oav/pin_image_recognition/__init__.py +6 -6
- dodal/devices/oav/pin_image_recognition/utils.py +5 -6
- dodal/devices/oav/utils.py +2 -2
- dodal/devices/p99/__init__.py +0 -0
- dodal/devices/p99/sample_stage.py +43 -0
- dodal/devices/robot.py +31 -20
- dodal/devices/scatterguard.py +1 -1
- dodal/devices/scintillator.py +8 -5
- dodal/devices/slits.py +1 -1
- dodal/devices/smargon.py +4 -4
- dodal/devices/status.py +2 -31
- dodal/devices/tetramm.py +23 -19
- dodal/devices/thawer.py +5 -3
- dodal/devices/training_rig/__init__.py +0 -0
- dodal/devices/training_rig/sample_stage.py +10 -0
- dodal/devices/turbo_slit.py +1 -1
- dodal/devices/undulator.py +1 -1
- dodal/devices/undulator_dcm.py +6 -8
- dodal/devices/util/adjuster_plans.py +3 -3
- dodal/devices/util/epics_util.py +5 -7
- dodal/devices/util/lookup_tables.py +2 -3
- dodal/devices/util/save_panda.py +87 -0
- dodal/devices/util/test_utils.py +17 -0
- dodal/devices/webcam.py +3 -3
- dodal/devices/xbpm_feedback.py +1 -25
- dodal/devices/xspress3/xspress3.py +1 -1
- dodal/devices/zebra.py +15 -10
- dodal/devices/zebra_controlled_shutter.py +26 -11
- dodal/devices/zocalo/zocalo_interaction.py +10 -2
- dodal/devices/zocalo/zocalo_results.py +36 -19
- dodal/log.py +46 -15
- dodal/plans/check_topup.py +65 -10
- dodal/plans/data_session_metadata.py +8 -9
- dodal/plans/motor_util_plans.py +117 -0
- dodal/utils.py +65 -22
- dls_dodal-1.29.4.dist-info/RECORD +0 -125
- dls_dodal-1.29.4.dist-info/entry_points.txt +0 -2
- dodal/devices/beamstop.py +0 -8
- dodal/devices/qbpm1.py +0 -8
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/top_level.txt +0 -0
dodal/devices/i22/nxsas.py
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Awaitable, Iterable
|
|
1
3
|
from dataclasses import dataclass, fields
|
|
2
|
-
from typing import
|
|
4
|
+
from typing import TypeVar
|
|
3
5
|
|
|
4
6
|
from bluesky.protocols import Reading
|
|
5
7
|
from event_model.documents.event_descriptor import DataKey
|
|
6
|
-
from ophyd_async.core import
|
|
7
|
-
from ophyd_async.epics.
|
|
8
|
-
from ophyd_async.epics.
|
|
8
|
+
from ophyd_async.core import PathProvider
|
|
9
|
+
from ophyd_async.epics.adaravis import AravisController, AravisDetector
|
|
10
|
+
from ophyd_async.epics.adpilatus import PilatusDetector
|
|
9
11
|
|
|
10
12
|
ValueAndUnits = tuple[float, str]
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# TODO: Remove this file as part of github.com/DiamondLightSource/dodal/issues/595
|
|
17
|
+
# Until which, temporarily duplicated non-public method from ophyd_async
|
|
18
|
+
async def _merge_gathered_dicts(
|
|
19
|
+
coros: Iterable[Awaitable[dict[str, T]]],
|
|
20
|
+
) -> dict[str, T]:
|
|
21
|
+
"""Merge dictionaries produced by a sequence of coroutines.
|
|
22
|
+
|
|
23
|
+
Can be used for merging ``read()`` or ``describe``. For instance::
|
|
24
|
+
|
|
25
|
+
combined_read = await merge_gathered_dicts(s.read() for s in signals)
|
|
26
|
+
"""
|
|
27
|
+
ret: dict[str, T] = {}
|
|
28
|
+
for result in await asyncio.gather(*coros):
|
|
29
|
+
ret.update(result)
|
|
30
|
+
return ret
|
|
11
31
|
|
|
12
32
|
|
|
13
33
|
@dataclass
|
|
14
34
|
class MetadataHolder:
|
|
15
35
|
# TODO: just in case this is useful more widely...
|
|
16
|
-
async def describe(self, parent_name: str) ->
|
|
36
|
+
async def describe(self, parent_name: str) -> dict[str, DataKey]:
|
|
17
37
|
def datakey(value) -> DataKey:
|
|
18
38
|
if isinstance(value, tuple):
|
|
19
39
|
return {"units": value[1], **datakey(value[0])}
|
|
@@ -40,8 +60,8 @@ class MetadataHolder:
|
|
|
40
60
|
if getattr(self, field.name, None) is not None
|
|
41
61
|
}
|
|
42
62
|
|
|
43
|
-
async def read(self, parent_name: str) ->
|
|
44
|
-
def reading(value):
|
|
63
|
+
async def read(self, parent_name: str) -> dict[str, Reading]:
|
|
64
|
+
def reading(value) -> Reading:
|
|
45
65
|
if isinstance(value, tuple):
|
|
46
66
|
return reading(value[0])
|
|
47
67
|
return {"timestamp": -1, "value": value}
|
|
@@ -81,7 +101,7 @@ class NXSasPilatus(PilatusDetector):
|
|
|
81
101
|
def __init__(
|
|
82
102
|
self,
|
|
83
103
|
prefix: str,
|
|
84
|
-
|
|
104
|
+
path_provider: PathProvider,
|
|
85
105
|
drv_suffix: str,
|
|
86
106
|
hdf_suffix: str,
|
|
87
107
|
metadata_holder: NXSasMetadataHolder,
|
|
@@ -94,32 +114,28 @@ class NXSasPilatus(PilatusDetector):
|
|
|
94
114
|
Writes hdf5 files."""
|
|
95
115
|
super().__init__(
|
|
96
116
|
prefix,
|
|
97
|
-
|
|
117
|
+
path_provider,
|
|
98
118
|
drv_suffix=drv_suffix,
|
|
99
119
|
hdf_suffix=hdf_suffix,
|
|
100
120
|
name=name,
|
|
101
121
|
)
|
|
102
122
|
self._metadata_holder = metadata_holder
|
|
103
123
|
|
|
104
|
-
async def read_configuration(self) ->
|
|
105
|
-
return await
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
self._metadata_holder.read(self.name),
|
|
111
|
-
)
|
|
124
|
+
async def read_configuration(self) -> dict[str, Reading]:
|
|
125
|
+
return await _merge_gathered_dicts(
|
|
126
|
+
r
|
|
127
|
+
for r in (
|
|
128
|
+
super().read_configuration(),
|
|
129
|
+
self._metadata_holder.read(self.name),
|
|
112
130
|
)
|
|
113
131
|
)
|
|
114
132
|
|
|
115
|
-
async def describe_configuration(self) ->
|
|
116
|
-
return await
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
self._metadata_holder.describe(self.name),
|
|
122
|
-
)
|
|
133
|
+
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
134
|
+
return await _merge_gathered_dicts(
|
|
135
|
+
r
|
|
136
|
+
for r in (
|
|
137
|
+
super().describe_configuration(),
|
|
138
|
+
self._metadata_holder.describe(self.name),
|
|
123
139
|
)
|
|
124
140
|
)
|
|
125
141
|
|
|
@@ -128,7 +144,7 @@ class NXSasOAV(AravisDetector):
|
|
|
128
144
|
def __init__(
|
|
129
145
|
self,
|
|
130
146
|
prefix: str,
|
|
131
|
-
|
|
147
|
+
path_provider: PathProvider,
|
|
132
148
|
drv_suffix: str,
|
|
133
149
|
hdf_suffix: str,
|
|
134
150
|
metadata_holder: NXSasMetadataHolder,
|
|
@@ -142,7 +158,7 @@ class NXSasOAV(AravisDetector):
|
|
|
142
158
|
Writes hdf5 files."""
|
|
143
159
|
super().__init__(
|
|
144
160
|
prefix,
|
|
145
|
-
|
|
161
|
+
path_provider,
|
|
146
162
|
drv_suffix=drv_suffix,
|
|
147
163
|
hdf_suffix=hdf_suffix,
|
|
148
164
|
name=name,
|
|
@@ -150,24 +166,20 @@ class NXSasOAV(AravisDetector):
|
|
|
150
166
|
)
|
|
151
167
|
self._metadata_holder = metadata_holder
|
|
152
168
|
|
|
153
|
-
async def read_configuration(self) ->
|
|
154
|
-
return await
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
self._metadata_holder.read(self.name),
|
|
160
|
-
)
|
|
169
|
+
async def read_configuration(self) -> dict[str, Reading]:
|
|
170
|
+
return await _merge_gathered_dicts(
|
|
171
|
+
r
|
|
172
|
+
for r in (
|
|
173
|
+
super().read_configuration(),
|
|
174
|
+
self._metadata_holder.read(self.name),
|
|
161
175
|
)
|
|
162
176
|
)
|
|
163
177
|
|
|
164
|
-
async def describe_configuration(self) ->
|
|
165
|
-
return await
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
self._metadata_holder.describe(self.name),
|
|
171
|
-
)
|
|
178
|
+
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
179
|
+
return await _merge_gathered_dicts(
|
|
180
|
+
r
|
|
181
|
+
for r in (
|
|
182
|
+
super().describe_configuration(),
|
|
183
|
+
self._metadata_holder.describe(self.name),
|
|
172
184
|
)
|
|
173
185
|
)
|
dodal/devices/i24/aperture.py
CHANGED
dodal/devices/i24/beamstop.py
CHANGED
dodal/devices/i24/dcm.py
CHANGED
dodal/devices/i24/pmac.py
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
from enum import Enum
|
|
1
|
+
from enum import Enum, IntEnum
|
|
2
|
+
from typing import SupportsFloat
|
|
2
3
|
|
|
3
4
|
from bluesky.protocols import Triggerable
|
|
4
|
-
from ophyd_async.core import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
DEFAULT_TIMEOUT,
|
|
7
|
+
AsyncStatus,
|
|
8
|
+
CalculateTimeout,
|
|
9
|
+
SignalBackend,
|
|
10
|
+
SignalR,
|
|
11
|
+
SignalRW,
|
|
12
|
+
SoftSignalBackend,
|
|
13
|
+
StandardReadable,
|
|
14
|
+
wait_for_value,
|
|
15
|
+
)
|
|
16
|
+
from ophyd_async.epics.motor import Motor
|
|
10
17
|
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
|
|
11
18
|
|
|
12
19
|
HOME_STR = r"\#1hmz\#2hmz\#3hmz" # Command to home the PMAC motors
|
|
13
20
|
ZERO_STR = "!x0y0z0" # Command to blend any ongoing move into new position
|
|
14
21
|
|
|
15
22
|
|
|
23
|
+
class ScanState(IntEnum):
|
|
24
|
+
RUNNING = 1
|
|
25
|
+
DONE = 0
|
|
26
|
+
|
|
27
|
+
|
|
16
28
|
class LaserSettings(str, Enum):
|
|
17
29
|
"""PMAC strings to switch laser on and off.
|
|
18
30
|
Note. On the PMAC, M-variables usually have to do with position compare
|
|
@@ -73,26 +85,65 @@ class PMACStringLaser(SignalRW):
|
|
|
73
85
|
super().__init__(backend, timeout, name)
|
|
74
86
|
|
|
75
87
|
@AsyncStatus.wrap
|
|
76
|
-
async def set(
|
|
77
|
-
|
|
88
|
+
async def set(
|
|
89
|
+
self,
|
|
90
|
+
value: LaserSettings,
|
|
91
|
+
wait=True,
|
|
92
|
+
timeout=CalculateTimeout,
|
|
93
|
+
):
|
|
94
|
+
await self.signal.set(value.value, wait, timeout)
|
|
78
95
|
|
|
79
96
|
|
|
80
97
|
class PMACStringEncReset(SignalRW):
|
|
81
|
-
""""""
|
|
98
|
+
"""Set a pmac_string to control the encoder channels in the controller."""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
pmac_str_sig: SignalRW,
|
|
103
|
+
backend: SignalBackend,
|
|
104
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
105
|
+
name: str = "",
|
|
106
|
+
) -> None:
|
|
107
|
+
self.signal = pmac_str_sig
|
|
108
|
+
super().__init__(backend, timeout, name)
|
|
109
|
+
|
|
110
|
+
@AsyncStatus.wrap
|
|
111
|
+
async def set(
|
|
112
|
+
self,
|
|
113
|
+
value: EncReset,
|
|
114
|
+
wait=True,
|
|
115
|
+
timeout=CalculateTimeout,
|
|
116
|
+
):
|
|
117
|
+
await self.signal.set(value.value, wait, timeout)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ProgramRunner(SignalRW):
|
|
121
|
+
"""Trigger the collection by setting the program number on the PMAC string.
|
|
122
|
+
|
|
123
|
+
Once the program number has been set, wait for the collection to be complete.
|
|
124
|
+
This will only be true when the status becomes 0.
|
|
125
|
+
"""
|
|
82
126
|
|
|
83
127
|
def __init__(
|
|
84
128
|
self,
|
|
85
129
|
pmac_str_sig: SignalRW,
|
|
130
|
+
status_sig: SignalR,
|
|
86
131
|
backend: SignalBackend,
|
|
87
132
|
timeout: float | None = DEFAULT_TIMEOUT,
|
|
88
133
|
name: str = "",
|
|
89
134
|
) -> None:
|
|
90
135
|
self.signal = pmac_str_sig
|
|
136
|
+
self.status = status_sig
|
|
91
137
|
super().__init__(backend, timeout, name)
|
|
92
138
|
|
|
93
139
|
@AsyncStatus.wrap
|
|
94
|
-
async def set(self,
|
|
95
|
-
|
|
140
|
+
async def set(self, value: int, wait=True, timeout=None):
|
|
141
|
+
prog_str = f"&2b{value}r"
|
|
142
|
+
assert isinstance(timeout, SupportsFloat) or (
|
|
143
|
+
timeout is None
|
|
144
|
+
), f"ProgramRunner does not support calculating timeout itself, {timeout=}"
|
|
145
|
+
await self.signal.set(prog_str, wait=wait)
|
|
146
|
+
await wait_for_value(self.status, ScanState.DONE, timeout)
|
|
96
147
|
|
|
97
148
|
|
|
98
149
|
class PMAC(StandardReadable):
|
|
@@ -121,4 +172,8 @@ class PMAC(StandardReadable):
|
|
|
121
172
|
self.scanstatus = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2401")
|
|
122
173
|
self.counter = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2402")
|
|
123
174
|
|
|
175
|
+
self.run_program = ProgramRunner(
|
|
176
|
+
self.pmac_string, self.scanstatus, backend=SoftSignalBackend(str)
|
|
177
|
+
)
|
|
178
|
+
|
|
124
179
|
super().__init__(name)
|
dodal/devices/ipin.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
1
|
+
from ophyd_async.core import HintedSignal, StandardReadable
|
|
2
|
+
from ophyd_async.epics.signal import epics_signal_r
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
class IPin(
|
|
5
|
+
class IPin(StandardReadable):
|
|
6
6
|
"""Simple device to get the ipin reading"""
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
9
|
+
with self.add_children_as_readables(wrapper=HintedSignal):
|
|
10
|
+
self.pin_readback = epics_signal_r(float, prefix + "I")
|
|
11
|
+
super().__init__(name)
|
dodal/devices/linkam3.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
from bluesky.protocols import Location
|
|
7
|
-
from ophyd_async.core import
|
|
8
|
-
|
|
6
|
+
from ophyd_async.core import (
|
|
7
|
+
ConfigSignal,
|
|
8
|
+
HintedSignal,
|
|
9
|
+
StandardReadable,
|
|
10
|
+
WatchableAsyncStatus,
|
|
11
|
+
WatcherUpdate,
|
|
12
|
+
observe_value,
|
|
13
|
+
)
|
|
9
14
|
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
|
|
10
15
|
|
|
11
16
|
|
|
@@ -57,14 +62,15 @@ class Linkam3(StandardReadable):
|
|
|
57
62
|
# status is a bitfield stored in a double?
|
|
58
63
|
self.status = epics_signal_r(float, prefix + "STATUS:")
|
|
59
64
|
|
|
60
|
-
self.
|
|
61
|
-
|
|
65
|
+
self.add_readables((self.temp,), wrapper=HintedSignal)
|
|
66
|
+
self.add_readables(
|
|
67
|
+
(self.ramp_rate, self.speed, self.set_point), wrapper=ConfigSignal
|
|
62
68
|
)
|
|
63
69
|
|
|
64
70
|
super().__init__(name=name)
|
|
65
71
|
|
|
66
72
|
@WatchableAsyncStatus.wrap
|
|
67
|
-
async def set(self, new_position: float, timeout:
|
|
73
|
+
async def set(self, new_position: float, timeout: float | None = None):
|
|
68
74
|
# time.monotonic won't go backwards in case of NTP corrections
|
|
69
75
|
start = time.monotonic()
|
|
70
76
|
old_position = await self.set_point.get_value()
|
|
@@ -3,7 +3,7 @@ from ophyd.log import logger as ophyd_logger
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class InfoLoggingDevice(Device):
|
|
6
|
-
def wait_for_connection(self, all_signals=False, timeout=2):
|
|
6
|
+
def wait_for_connection(self, all_signals=False, timeout=2.0):
|
|
7
7
|
class_name = self.__class__.__name__
|
|
8
8
|
ophyd_logger.info(
|
|
9
9
|
f"{class_name} waiting for connection, {'not' if all_signals else ''} waiting for all signals, timeout = {timeout}s.",
|
dodal/devices/motors.py
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
from ophyd_async.core import Device
|
|
2
|
-
from ophyd_async.epics.
|
|
2
|
+
from ophyd_async.epics.motor import Motor
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class XYZPositioner(Device):
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
Standard ophyd_async xyz motor stage, by combining 3 Motors,
|
|
9
|
+
with added infix for extra flexibliy to allow different axes other than x,y,z.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
prefix:
|
|
14
|
+
EPICS PV (Common part up to and including :).
|
|
15
|
+
name:
|
|
16
|
+
name for the stage.
|
|
17
|
+
infix:
|
|
18
|
+
EPICS PV, default is the ["X", "Y", "Z"].
|
|
19
|
+
Notes
|
|
20
|
+
-----
|
|
21
|
+
Example usage::
|
|
22
|
+
async with DeviceCollector():
|
|
23
|
+
xyz_stage = XYZPositioner("BLXX-MO-STAGE-XX:")
|
|
24
|
+
Or::
|
|
25
|
+
with DeviceCollector():
|
|
26
|
+
xyz_stage = XYZPositioner("BLXX-MO-STAGE-XX:", suffix = ["A", "B", "C"])
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, prefix: str, name: str, infix: list[str] | None = None):
|
|
31
|
+
if infix is None:
|
|
32
|
+
infix = ["X", "Y", "Z"]
|
|
33
|
+
self.x = Motor(prefix + infix[0])
|
|
34
|
+
self.y = Motor(prefix + infix[1])
|
|
35
|
+
self.z = Motor(prefix + infix[2])
|
|
36
|
+
super().__init__(name=name)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# type: ignore # OAV will soon be ophyd-async, see https://github.com/DiamondLightSource/dodal/issues/716
|
|
1
2
|
from functools import partial
|
|
2
3
|
|
|
3
4
|
from ophyd import ADComponent as ADC
|
|
@@ -46,7 +47,7 @@ class ZoomController(Device):
|
|
|
46
47
|
sxst = Component(EpicsSignal, "MP:SELECT.SXST")
|
|
47
48
|
|
|
48
49
|
def set_flatfield_on_zoom_level_one(self, value):
|
|
49
|
-
self.parent:
|
|
50
|
+
self.parent: OAV
|
|
50
51
|
flat_applied = self.parent.proc.port_name.get()
|
|
51
52
|
no_flat_applied = self.parent.cam.port_name.get()
|
|
52
53
|
return self.parent.grid_snapshot.input_plugin.set(
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import xml.etree.
|
|
2
|
+
import xml.etree.ElementTree as et
|
|
3
3
|
from collections import ChainMap
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
|
+
from xml.etree.ElementTree import Element
|
|
5
6
|
|
|
6
7
|
from dodal.devices.oav.oav_errors import (
|
|
7
8
|
OAVError_BeamPositionNotFound,
|
|
@@ -20,6 +21,13 @@ OAV_CONFIG_JSON = (
|
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def _get_element_as_float(node: Element, element_name: str) -> float:
|
|
25
|
+
element = node.find(element_name)
|
|
26
|
+
assert element is not None, f"{element_name} not found in {node}"
|
|
27
|
+
assert element.text
|
|
28
|
+
return float(element.text)
|
|
29
|
+
|
|
30
|
+
|
|
23
31
|
class OAVParameters:
|
|
24
32
|
"""
|
|
25
33
|
The parameters to set up the OAV depending on the context.
|
|
@@ -65,11 +73,11 @@ class OAVParameters:
|
|
|
65
73
|
try:
|
|
66
74
|
param = param_type(param)
|
|
67
75
|
return param
|
|
68
|
-
except AssertionError:
|
|
76
|
+
except AssertionError as e:
|
|
69
77
|
raise TypeError(
|
|
70
78
|
f"OAV param {name} from the OAV centring params json file has the "
|
|
71
79
|
f"wrong type, should be {param_type} but is {type(param)}."
|
|
72
|
-
)
|
|
80
|
+
) from e
|
|
73
81
|
|
|
74
82
|
self.exposure: float = update("exposure", float)
|
|
75
83
|
self.acquire_period: float = update("acqPeriod", float)
|
|
@@ -134,14 +142,14 @@ class OAVConfigParams:
|
|
|
134
142
|
root = tree.getroot()
|
|
135
143
|
levels = root.findall(".//zoomLevel")
|
|
136
144
|
for node in levels:
|
|
137
|
-
if
|
|
145
|
+
if _get_element_as_float(node, "level") == zoom:
|
|
138
146
|
self.micronsPerXPixel = (
|
|
139
|
-
|
|
147
|
+
_get_element_as_float(node, "micronsPerXPixel")
|
|
140
148
|
* DEFAULT_OAV_WINDOW[0]
|
|
141
149
|
/ xsize
|
|
142
150
|
)
|
|
143
151
|
self.micronsPerYPixel = (
|
|
144
|
-
|
|
152
|
+
_get_element_as_float(node, "micronsPerYPixel")
|
|
145
153
|
* DEFAULT_OAV_WINDOW[1]
|
|
146
154
|
/ ysize
|
|
147
155
|
)
|
|
@@ -155,7 +163,7 @@ class OAVConfigParams:
|
|
|
155
163
|
|
|
156
164
|
def get_beam_position_from_zoom(
|
|
157
165
|
self, zoom: float, xsize: int, ysize: int
|
|
158
|
-
) ->
|
|
166
|
+
) -> tuple[int, int]:
|
|
159
167
|
"""
|
|
160
168
|
Extracts the beam location in pixels `xCentre` `yCentre`, for a requested zoom \
|
|
161
169
|
level. The beam location is manually inputted by the beamline operator on GDA \
|
|
@@ -164,7 +172,7 @@ class OAVConfigParams:
|
|
|
164
172
|
"""
|
|
165
173
|
crosshair_x_line = None
|
|
166
174
|
crosshair_y_line = None
|
|
167
|
-
with open(self.display_config
|
|
175
|
+
with open(self.display_config) as f:
|
|
168
176
|
file_lines = f.readlines()
|
|
169
177
|
for i in range(len(file_lines)):
|
|
170
178
|
if file_lines[i].startswith("zoomLevel = " + str(zoom)):
|
|
@@ -188,7 +196,7 @@ class OAVConfigParams:
|
|
|
188
196
|
|
|
189
197
|
def calculate_beam_distance(
|
|
190
198
|
self, horizontal_pixels: int, vertical_pixels: int
|
|
191
|
-
) ->
|
|
199
|
+
) -> tuple[int, int]:
|
|
192
200
|
"""
|
|
193
201
|
Calculates the distance between the beam centre and the given (horizontal, vertical).
|
|
194
202
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import io
|
|
3
|
+
import pickle
|
|
4
|
+
import uuid
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from aiohttp import ClientResponse, ClientSession
|
|
10
|
+
from bluesky.protocols import Flyable, Stoppable
|
|
11
|
+
from ophyd_async.core import (
|
|
12
|
+
AsyncStatus,
|
|
13
|
+
StandardReadable,
|
|
14
|
+
soft_signal_r_and_setter,
|
|
15
|
+
soft_signal_rw,
|
|
16
|
+
)
|
|
17
|
+
from ophyd_async.epics.signal import epics_signal_r
|
|
18
|
+
from PIL import Image
|
|
19
|
+
from redis.asyncio import StrictRedis
|
|
20
|
+
|
|
21
|
+
from dodal.log import LOGGER
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def get_next_jpeg(response: ClientResponse) -> bytes:
|
|
25
|
+
JPEG_START_BYTE = b"\xff\xd8"
|
|
26
|
+
JPEG_STOP_BYTE = b"\xff\xd9"
|
|
27
|
+
while True:
|
|
28
|
+
line = await response.content.readline()
|
|
29
|
+
if line.startswith(JPEG_START_BYTE):
|
|
30
|
+
return line + await response.content.readuntil(JPEG_STOP_BYTE)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
|
|
34
|
+
"""Forwards OAV image data to redis. To use call:
|
|
35
|
+
|
|
36
|
+
> bps.kickoff(oav_forwarder)
|
|
37
|
+
> bps.monitor(oav_forwarder.uuid)
|
|
38
|
+
> bps.complete(oav_forwarder)
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
DATA_EXPIRY_DAYS = 7
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
prefix: str,
|
|
47
|
+
redis_host: str,
|
|
48
|
+
redis_password: str,
|
|
49
|
+
redis_db: int = 0,
|
|
50
|
+
name: str = "",
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Reads image data from the MJPEG stream on an OAV and forwards it into a
|
|
53
|
+
redis database. This is currently only used for murko integration.
|
|
54
|
+
|
|
55
|
+
Arguments:
|
|
56
|
+
prefix: str the PV prefix of the OAV
|
|
57
|
+
redis_host: str the host where the redis database is running
|
|
58
|
+
redis_password: str the password for the redis database
|
|
59
|
+
redis_db: int which redis database to connect to, defaults to 0
|
|
60
|
+
name: str the name of this device
|
|
61
|
+
"""
|
|
62
|
+
self.stream_url = epics_signal_r(str, f"{prefix}MJPG:MJPG_URL_RBV")
|
|
63
|
+
|
|
64
|
+
with self.add_children_as_readables():
|
|
65
|
+
self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
|
|
66
|
+
|
|
67
|
+
self.forwarding_task = None
|
|
68
|
+
self.redis_client = StrictRedis(
|
|
69
|
+
host=redis_host, password=redis_password, db=redis_db
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self._stop_flag = False
|
|
73
|
+
|
|
74
|
+
self.sample_id = soft_signal_rw(int, initial_value=0)
|
|
75
|
+
|
|
76
|
+
# The uuid that images are being saved under, this should be monitored for
|
|
77
|
+
# callbacks to correlate the data
|
|
78
|
+
self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
|
|
79
|
+
|
|
80
|
+
super().__init__(name=name)
|
|
81
|
+
|
|
82
|
+
async def _get_frame_and_put_to_redis(self, response: ClientResponse):
|
|
83
|
+
"""Converts the data that comes in as a jpeg byte stream into a numpy array of
|
|
84
|
+
RGB values, pickles this array then writes it to redis.
|
|
85
|
+
"""
|
|
86
|
+
jpeg_bytes = await get_next_jpeg(response)
|
|
87
|
+
self.uuid_setter(image_uuid := str(uuid.uuid4()))
|
|
88
|
+
img = Image.open(io.BytesIO(jpeg_bytes))
|
|
89
|
+
image_data = pickle.dumps(np.asarray(img))
|
|
90
|
+
sample_id = str(await self.sample_id.get_value())
|
|
91
|
+
await self.redis_client.hset(sample_id, image_uuid, image_data) # type: ignore
|
|
92
|
+
await self.redis_client.expire(sample_id, timedelta(days=self.DATA_EXPIRY_DAYS))
|
|
93
|
+
LOGGER.debug(f"Sent frame to redis key {sample_id} with uuid {image_uuid}")
|
|
94
|
+
|
|
95
|
+
async def _open_connection_and_do_function(
|
|
96
|
+
self, function_to_do: Callable[[ClientResponse, str | None], Awaitable]
|
|
97
|
+
):
|
|
98
|
+
stream_url = await self.stream_url.get_value()
|
|
99
|
+
async with ClientSession() as session:
|
|
100
|
+
async with session.get(stream_url) as response:
|
|
101
|
+
await function_to_do(response, stream_url)
|
|
102
|
+
|
|
103
|
+
async def _stream_to_redis(self, response, _):
|
|
104
|
+
while not self._stop_flag:
|
|
105
|
+
await self._get_frame_and_put_to_redis(response)
|
|
106
|
+
await asyncio.sleep(0.01)
|
|
107
|
+
|
|
108
|
+
async def _confirm_mjpg_stream(self, response, stream_url):
|
|
109
|
+
if response.content_type != "multipart/x-mixed-replace":
|
|
110
|
+
raise ValueError(f"{stream_url} is not an MJPG stream")
|
|
111
|
+
|
|
112
|
+
@AsyncStatus.wrap
|
|
113
|
+
async def kickoff(self):
|
|
114
|
+
self._stop_flag = False
|
|
115
|
+
await self._open_connection_and_do_function(self._confirm_mjpg_stream)
|
|
116
|
+
self.forwarding_task = asyncio.create_task(
|
|
117
|
+
self._open_connection_and_do_function(self._stream_to_redis)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@AsyncStatus.wrap
|
|
121
|
+
async def complete(self):
|
|
122
|
+
assert self.forwarding_task, "Device not kicked off"
|
|
123
|
+
await self.stop()
|
|
124
|
+
|
|
125
|
+
@AsyncStatus.wrap
|
|
126
|
+
async def stop(self, success=True):
|
|
127
|
+
if self.forwarding_task:
|
|
128
|
+
self._stop_flag = True
|
|
129
|
+
await self.forwarding_task
|