dls-dodal 1.29.3__py3-none-any.whl → 1.30.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.3.dist-info → dls_dodal-1.30.0.dist-info}/METADATA +28 -43
- dls_dodal-1.30.0.dist-info/RECORD +132 -0
- {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/WHEEL +1 -1
- dls_dodal-1.30.0.dist-info/entry_points.txt +3 -0
- dodal/__init__.py +1 -4
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +3 -1
- dodal/beamlines/i03.py +28 -23
- dodal/beamlines/i04.py +34 -12
- dodal/beamlines/i13_1.py +66 -0
- dodal/beamlines/i22.py +5 -5
- dodal/beamlines/i24.py +15 -1
- dodal/beamlines/p38.py +7 -7
- dodal/beamlines/p45.py +7 -5
- dodal/beamlines/p99.py +61 -0
- dodal/cli.py +6 -3
- dodal/common/beamlines/beamline_parameters.py +2 -2
- dodal/common/beamlines/beamline_utils.py +6 -5
- dodal/common/maths.py +1 -3
- dodal/common/types.py +2 -3
- dodal/common/udc_directory_provider.py +14 -3
- dodal/common/visit.py +2 -3
- dodal/devices/CTAB.py +22 -17
- dodal/devices/aperturescatterguard.py +114 -136
- 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 +0 -4
- dodal/devices/attenuator.py +4 -4
- dodal/devices/cryostream.py +19 -7
- 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 +8 -7
- dodal/devices/detector/detector_motion.py +38 -31
- dodal/devices/eiger.py +23 -23
- dodal/devices/eiger_odin.py +12 -13
- dodal/devices/fast_grid_scan.py +4 -3
- dodal/devices/fluorescence_detector_motion.py +13 -4
- dodal/devices/focusing_mirror.py +66 -66
- dodal/devices/hutch_shutter.py +4 -4
- dodal/devices/i22/dcm.py +4 -3
- dodal/devices/i22/fswitch.py +4 -4
- dodal/devices/i22/nxsas.py +23 -32
- dodal/devices/i24/dcm.py +42 -0
- dodal/devices/i24/pmac.py +47 -8
- dodal/devices/ipin.py +7 -4
- dodal/devices/linkam3.py +11 -5
- dodal/devices/logging_ophyd_device.py +1 -1
- dodal/devices/motors.py +31 -5
- 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 -0
- dodal/devices/oav/oav_parameters.py +18 -10
- dodal/devices/oav/oav_to_redis_forwarder.py +100 -0
- dodal/devices/oav/pin_image_recognition/__init__.py +19 -17
- dodal/devices/oav/pin_image_recognition/utils.py +5 -6
- dodal/devices/oav/utils.py +3 -17
- dodal/devices/p99/__init__.py +0 -0
- dodal/devices/p99/sample_stage.py +43 -0
- dodal/devices/robot.py +30 -18
- dodal/devices/scintillator.py +8 -5
- dodal/devices/smargon.py +3 -3
- dodal/devices/status.py +2 -31
- dodal/devices/tetramm.py +4 -4
- dodal/devices/thawer.py +5 -3
- dodal/devices/undulator_dcm.py +6 -8
- dodal/devices/util/adjuster_plans.py +2 -2
- dodal/devices/util/epics_util.py +6 -8
- 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 -8
- dodal/devices/xbpm_feedback.py +0 -23
- dodal/devices/zebra.py +10 -10
- dodal/devices/zebra_controlled_shutter.py +3 -3
- dodal/devices/zocalo/zocalo_interaction.py +10 -2
- dodal/devices/zocalo/zocalo_results.py +31 -18
- dodal/log.py +14 -5
- dodal/plans/data_session_metadata.py +1 -0
- dodal/plans/motor_util_plans.py +117 -0
- dodal/utils.py +74 -26
- dls_dodal-1.29.3.dist-info/RECORD +0 -124
- dls_dodal-1.29.3.dist-info/entry_points.txt +0 -2
- dodal/devices/qbpm1.py +0 -8
- {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +0 -0
dodal/devices/i22/nxsas.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from dataclasses import dataclass, fields
|
|
2
|
-
from typing import Dict
|
|
3
2
|
|
|
4
3
|
from bluesky.protocols import Reading
|
|
5
4
|
from event_model.documents.event_descriptor import DataKey
|
|
@@ -13,7 +12,7 @@ ValueAndUnits = tuple[float, str]
|
|
|
13
12
|
@dataclass
|
|
14
13
|
class MetadataHolder:
|
|
15
14
|
# TODO: just in case this is useful more widely...
|
|
16
|
-
async def describe(self, parent_name: str) ->
|
|
15
|
+
async def describe(self, parent_name: str) -> dict[str, DataKey]:
|
|
17
16
|
def datakey(value) -> DataKey:
|
|
18
17
|
if isinstance(value, tuple):
|
|
19
18
|
return {"units": value[1], **datakey(value[0])}
|
|
@@ -40,8 +39,8 @@ class MetadataHolder:
|
|
|
40
39
|
if getattr(self, field.name, None) is not None
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
async def read(self, parent_name: str) ->
|
|
44
|
-
def reading(value):
|
|
42
|
+
async def read(self, parent_name: str) -> dict[str, Reading]:
|
|
43
|
+
def reading(value) -> Reading:
|
|
45
44
|
if isinstance(value, tuple):
|
|
46
45
|
return reading(value[0])
|
|
47
46
|
return {"timestamp": -1, "value": value}
|
|
@@ -101,25 +100,21 @@ class NXSasPilatus(PilatusDetector):
|
|
|
101
100
|
)
|
|
102
101
|
self._metadata_holder = metadata_holder
|
|
103
102
|
|
|
104
|
-
async def read_configuration(self) ->
|
|
103
|
+
async def read_configuration(self) -> dict[str, Reading]:
|
|
105
104
|
return await merge_gathered_dicts(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
self._metadata_holder.read(self.name),
|
|
111
|
-
)
|
|
105
|
+
r
|
|
106
|
+
for r in (
|
|
107
|
+
super().read_configuration(),
|
|
108
|
+
self._metadata_holder.read(self.name),
|
|
112
109
|
)
|
|
113
110
|
)
|
|
114
111
|
|
|
115
|
-
async def describe_configuration(self) ->
|
|
112
|
+
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
116
113
|
return await merge_gathered_dicts(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
self._metadata_holder.describe(self.name),
|
|
122
|
-
)
|
|
114
|
+
r
|
|
115
|
+
for r in (
|
|
116
|
+
super().describe_configuration(),
|
|
117
|
+
self._metadata_holder.describe(self.name),
|
|
123
118
|
)
|
|
124
119
|
)
|
|
125
120
|
|
|
@@ -150,24 +145,20 @@ class NXSasOAV(AravisDetector):
|
|
|
150
145
|
)
|
|
151
146
|
self._metadata_holder = metadata_holder
|
|
152
147
|
|
|
153
|
-
async def read_configuration(self) ->
|
|
148
|
+
async def read_configuration(self) -> dict[str, Reading]:
|
|
154
149
|
return await merge_gathered_dicts(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
self._metadata_holder.read(self.name),
|
|
160
|
-
)
|
|
150
|
+
r
|
|
151
|
+
for r in (
|
|
152
|
+
super().read_configuration(),
|
|
153
|
+
self._metadata_holder.read(self.name),
|
|
161
154
|
)
|
|
162
155
|
)
|
|
163
156
|
|
|
164
|
-
async def describe_configuration(self) ->
|
|
157
|
+
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
165
158
|
return await merge_gathered_dicts(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
self._metadata_holder.describe(self.name),
|
|
171
|
-
)
|
|
159
|
+
r
|
|
160
|
+
for r in (
|
|
161
|
+
super().describe_configuration(),
|
|
162
|
+
self._metadata_holder.describe(self.name),
|
|
172
163
|
)
|
|
173
164
|
)
|
dodal/devices/i24/dcm.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable
|
|
2
|
+
from ophyd_async.epics.motion import Motor
|
|
3
|
+
from ophyd_async.epics.signal import epics_signal_r
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DCM(StandardReadable):
|
|
7
|
+
"""
|
|
8
|
+
A double crystal monocromator device, used to select the beam energy.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
12
|
+
with self.add_children_as_readables():
|
|
13
|
+
# Motors
|
|
14
|
+
self.bragg_in_degrees = Motor(prefix + "-MO-DCM-01:BRAGG")
|
|
15
|
+
self.x_translation_in_mm = Motor(prefix + "-MO-DCM-01:X")
|
|
16
|
+
self.offset_in_mm = Motor(prefix + "-MO-DCM-01:OFFSET")
|
|
17
|
+
self.gap_in_mm = Motor(prefix + "-MO-DCM-01:GAP")
|
|
18
|
+
self.energy_in_kev = Motor(prefix + "-MO-DCM-01:ENERGY")
|
|
19
|
+
self.xtal1_roll = Motor(prefix + "-MO-DCM-01:XTAL1:ROLL")
|
|
20
|
+
self.xtal2_roll = Motor(prefix + "-MO-DCM-01:XTAL2:ROLL")
|
|
21
|
+
self.xtal2_pitch = Motor(prefix + "-MO-DCM-01:XTAL2:PITCH")
|
|
22
|
+
|
|
23
|
+
# Wavelength is calculated in epics from the energy
|
|
24
|
+
self.wavelength_in_a = epics_signal_r(float, prefix + "-MO-DCM-01:LAMBDA")
|
|
25
|
+
|
|
26
|
+
# Temperatures
|
|
27
|
+
self.xtal1_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-1")
|
|
28
|
+
self.xtal1_heater_temp = epics_signal_r(
|
|
29
|
+
float, prefix + "-DI-DCM-01:PT100-2"
|
|
30
|
+
)
|
|
31
|
+
self.xtal2_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-4")
|
|
32
|
+
self.xtal2_heater_temp = epics_signal_r(
|
|
33
|
+
float, prefix + "-DI-DCM-01:PT100-5"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.roll_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-3")
|
|
37
|
+
self.pitch_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-6")
|
|
38
|
+
self.backplate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-7")
|
|
39
|
+
self.b1_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-7")
|
|
40
|
+
self.gap_temp = epics_signal_r(float, prefix + "-DI-DCM-01:TC-1")
|
|
41
|
+
|
|
42
|
+
super().__init__(name)
|
dodal/devices/i24/pmac.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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 AsyncStatus, StandardReadable
|
|
5
|
-
from ophyd_async.core.signal import SignalRW
|
|
5
|
+
from ophyd_async.core import AsyncStatus, StandardReadable, wait_for_value
|
|
6
|
+
from ophyd_async.core.signal import CalculateTimeout, SignalR, SignalRW
|
|
6
7
|
from ophyd_async.core.signal_backend import SignalBackend
|
|
7
8
|
from ophyd_async.core.soft_signal_backend import SoftSignalBackend
|
|
8
9
|
from ophyd_async.core.utils import DEFAULT_TIMEOUT
|
|
@@ -13,6 +14,11 @@ HOME_STR = r"\#1hmz\#2hmz\#3hmz" # Command to home the PMAC motors
|
|
|
13
14
|
ZERO_STR = "!x0y0z0" # Command to blend any ongoing move into new position
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
class ScanState(IntEnum):
|
|
18
|
+
RUNNING = 1
|
|
19
|
+
DONE = 0
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
class LaserSettings(str, Enum):
|
|
17
23
|
"""PMAC strings to switch laser on and off.
|
|
18
24
|
Note. On the PMAC, M-variables usually have to do with position compare
|
|
@@ -73,26 +79,55 @@ class PMACStringLaser(SignalRW):
|
|
|
73
79
|
super().__init__(backend, timeout, name)
|
|
74
80
|
|
|
75
81
|
@AsyncStatus.wrap
|
|
76
|
-
async def set(self,
|
|
77
|
-
await self.signal.set(
|
|
82
|
+
async def set(self, value: LaserSettings, wait=True, timeout=CalculateTimeout):
|
|
83
|
+
await self.signal.set(value.value, wait, timeout)
|
|
78
84
|
|
|
79
85
|
|
|
80
86
|
class PMACStringEncReset(SignalRW):
|
|
81
|
-
""""""
|
|
87
|
+
"""Set a pmac_string to control the encoder channels in the controller."""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
pmac_str_sig: SignalRW,
|
|
92
|
+
backend: SignalBackend,
|
|
93
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
94
|
+
name: str = "",
|
|
95
|
+
) -> None:
|
|
96
|
+
self.signal = pmac_str_sig
|
|
97
|
+
super().__init__(backend, timeout, name)
|
|
98
|
+
|
|
99
|
+
@AsyncStatus.wrap
|
|
100
|
+
async def set(self, value: EncReset, wait=True, timeout=CalculateTimeout):
|
|
101
|
+
await self.signal.set(value.value, wait, timeout)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ProgramRunner(SignalRW):
|
|
105
|
+
"""Trigger the collection by setting the program number on the PMAC string.
|
|
106
|
+
|
|
107
|
+
Once the program number has been set, wait for the collection to be complete.
|
|
108
|
+
This will only be true when the status becomes 0.
|
|
109
|
+
"""
|
|
82
110
|
|
|
83
111
|
def __init__(
|
|
84
112
|
self,
|
|
85
113
|
pmac_str_sig: SignalRW,
|
|
114
|
+
status_sig: SignalR,
|
|
86
115
|
backend: SignalBackend,
|
|
87
116
|
timeout: float | None = DEFAULT_TIMEOUT,
|
|
88
117
|
name: str = "",
|
|
89
118
|
) -> None:
|
|
90
119
|
self.signal = pmac_str_sig
|
|
120
|
+
self.status = status_sig
|
|
91
121
|
super().__init__(backend, timeout, name)
|
|
92
122
|
|
|
93
123
|
@AsyncStatus.wrap
|
|
94
|
-
async def set(self,
|
|
95
|
-
|
|
124
|
+
async def set(self, value: int, wait=True, timeout=None):
|
|
125
|
+
prog_str = f"&2b{value}r"
|
|
126
|
+
assert isinstance(timeout, SupportsFloat) or (
|
|
127
|
+
timeout is None
|
|
128
|
+
), f"ProgramRunner does not support calculating timeout itself, {timeout=}"
|
|
129
|
+
await self.signal.set(prog_str, wait=wait)
|
|
130
|
+
await wait_for_value(self.status, ScanState.DONE, timeout)
|
|
96
131
|
|
|
97
132
|
|
|
98
133
|
class PMAC(StandardReadable):
|
|
@@ -121,4 +156,8 @@ class PMAC(StandardReadable):
|
|
|
121
156
|
self.scanstatus = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2401")
|
|
122
157
|
self.counter = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2402")
|
|
123
158
|
|
|
159
|
+
self.run_program = ProgramRunner(
|
|
160
|
+
self.pmac_string, self.scanstatus, backend=SoftSignalBackend(str)
|
|
161
|
+
)
|
|
162
|
+
|
|
124
163
|
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,10 +1,15 @@
|
|
|
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
|
|
6
|
+
from ophyd_async.core import (
|
|
7
|
+
ConfigSignal,
|
|
8
|
+
HintedSignal,
|
|
9
|
+
StandardReadable,
|
|
10
|
+
WatchableAsyncStatus,
|
|
11
|
+
observe_value,
|
|
12
|
+
)
|
|
8
13
|
from ophyd_async.core.utils import WatcherUpdate
|
|
9
14
|
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
|
|
10
15
|
|
|
@@ -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
|
@@ -3,8 +3,34 @@ from ophyd_async.epics.motion 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,6 +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):
|
|
50
|
+
self.parent: OAV
|
|
49
51
|
flat_applied = self.parent.proc.port_name.get()
|
|
50
52
|
no_flat_applied = self.parent.cam.port_name.get()
|
|
51
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,100 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import io
|
|
3
|
+
import pickle
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from aiohttp import ClientResponse, ClientSession
|
|
8
|
+
from bluesky.protocols import Flyable
|
|
9
|
+
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
10
|
+
from ophyd_async.core.signal import soft_signal_r_and_setter
|
|
11
|
+
from ophyd_async.epics.signal import epics_signal_r
|
|
12
|
+
from PIL import Image
|
|
13
|
+
from redis.asyncio import StrictRedis
|
|
14
|
+
|
|
15
|
+
from dodal.log import LOGGER
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def get_next_jpeg(response: ClientResponse) -> bytes:
|
|
19
|
+
JPEG_START_BYTE = b"\xff\xd8"
|
|
20
|
+
JPEG_STOP_BYTE = b"\xff\xd9"
|
|
21
|
+
while True:
|
|
22
|
+
line = await response.content.readline()
|
|
23
|
+
if line.startswith(JPEG_START_BYTE):
|
|
24
|
+
return line + await response.content.readuntil(JPEG_STOP_BYTE)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OAVToRedisForwarder(StandardReadable, Flyable):
|
|
28
|
+
"""Forwards OAV image data to redis. To use call:
|
|
29
|
+
|
|
30
|
+
> bps.kickoff(oav_forwarder)
|
|
31
|
+
> bps.monitor(oav_forwarder.uuid)
|
|
32
|
+
> bps.complete(oav_forwarder)
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
prefix: str,
|
|
39
|
+
redis_host: str,
|
|
40
|
+
redis_password: str,
|
|
41
|
+
redis_db: int = 0,
|
|
42
|
+
name: str = "",
|
|
43
|
+
redis_key: str = "test-image",
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Reads image data from the MJPEG stream on an OAV and forwards it into a
|
|
46
|
+
redis database. This is currently only used for murko integration.
|
|
47
|
+
|
|
48
|
+
Arguments:
|
|
49
|
+
prefix: str the PV prefix of the OAV
|
|
50
|
+
redis_host: str the host where the redis database is running
|
|
51
|
+
redis_password: str the password for the redis database
|
|
52
|
+
redis_db: int which redis database to connect to, defaults to 0
|
|
53
|
+
name: str the name of this device
|
|
54
|
+
redis_key: str the key to store data in, defaults to "test-image"
|
|
55
|
+
"""
|
|
56
|
+
self.stream_url = epics_signal_r(str, f"{prefix}-DI-OAV-01:MJPG:HOST_RBV")
|
|
57
|
+
|
|
58
|
+
with self.add_children_as_readables():
|
|
59
|
+
self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
|
|
60
|
+
|
|
61
|
+
self.forwarding_task = None
|
|
62
|
+
self.redis_client = StrictRedis(
|
|
63
|
+
host=redis_host, password=redis_password, db=redis_db
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.redis_key = redis_key
|
|
67
|
+
|
|
68
|
+
# The uuid that images are being saved under, this should be monitored for
|
|
69
|
+
# callbacks to correlate the data
|
|
70
|
+
self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
|
|
71
|
+
|
|
72
|
+
super().__init__(name=name)
|
|
73
|
+
|
|
74
|
+
async def _get_frame_and_put_to_redis(self, response: ClientResponse):
|
|
75
|
+
"""Converts the data that comes in as a jpeg byte stream into a numpy array of
|
|
76
|
+
RGB values, pickles this array then writes it to redis.
|
|
77
|
+
"""
|
|
78
|
+
jpeg_bytes = await get_next_jpeg(response)
|
|
79
|
+
self.uuid_setter(image_uuid := str(uuid.uuid4()))
|
|
80
|
+
img = Image.open(io.BytesIO(jpeg_bytes))
|
|
81
|
+
image_data = pickle.dumps(np.asarray(img))
|
|
82
|
+
await self.redis_client.hset(self.redis_key, image_uuid, image_data) # type: ignore
|
|
83
|
+
LOGGER.debug(f"Sent frame to redis key {self.redis_key} with uuid {image_uuid}")
|
|
84
|
+
|
|
85
|
+
async def _stream_to_redis(self):
|
|
86
|
+
stream_url = await self.stream_url.get_value()
|
|
87
|
+
async with ClientSession() as session:
|
|
88
|
+
async with session.get(stream_url) as response:
|
|
89
|
+
while True:
|
|
90
|
+
await self._get_frame_and_put_to_redis(response)
|
|
91
|
+
await asyncio.sleep(0.01)
|
|
92
|
+
|
|
93
|
+
@AsyncStatus.wrap
|
|
94
|
+
async def kickoff(self):
|
|
95
|
+
self.forwarding_task = asyncio.create_task(self._stream_to_redis())
|
|
96
|
+
|
|
97
|
+
@AsyncStatus.wrap
|
|
98
|
+
async def complete(self):
|
|
99
|
+
assert self.forwarding_task, "Device not kicked off"
|
|
100
|
+
self.forwarding_task.cancel()
|
|
@@ -5,6 +5,7 @@ import numpy as np
|
|
|
5
5
|
from numpy.typing import NDArray
|
|
6
6
|
from ophyd_async.core import (
|
|
7
7
|
AsyncStatus,
|
|
8
|
+
HintedSignal,
|
|
8
9
|
StandardReadable,
|
|
9
10
|
observe_value,
|
|
10
11
|
soft_signal_r_and_setter,
|
|
@@ -50,11 +51,13 @@ class PinTipDetection(StandardReadable):
|
|
|
50
51
|
self._prefix: str = prefix
|
|
51
52
|
self._name = name
|
|
52
53
|
|
|
53
|
-
self.triggered_tip,
|
|
54
|
-
|
|
54
|
+
self.triggered_tip, self._tip_setter = soft_signal_r_and_setter(
|
|
55
|
+
Tip, name="triggered_tip"
|
|
56
|
+
)
|
|
57
|
+
self.triggered_top_edge, self._top_edge_setter = soft_signal_r_and_setter(
|
|
55
58
|
NDArray[np.uint32], name="triggered_top_edge"
|
|
56
59
|
)
|
|
57
|
-
self.triggered_bottom_edge,
|
|
60
|
+
self.triggered_bottom_edge, self._bottom_edge_setter = soft_signal_r_and_setter(
|
|
58
61
|
NDArray[np.uint32], name="triggered_bottom_edge"
|
|
59
62
|
)
|
|
60
63
|
self.array_data = epics_signal_r(NDArray[np.uint8], f"pva://{prefix}PVA:ARRAY")
|
|
@@ -75,24 +78,25 @@ class PinTipDetection(StandardReadable):
|
|
|
75
78
|
self.min_tip_height = soft_signal_rw(int, 5, name="min_tip_height")
|
|
76
79
|
self.validity_timeout = soft_signal_rw(float, 5.0, name="validity_timeout")
|
|
77
80
|
|
|
78
|
-
self.
|
|
79
|
-
|
|
81
|
+
self.add_readables(
|
|
82
|
+
[
|
|
80
83
|
self.triggered_tip,
|
|
81
84
|
self.triggered_top_edge,
|
|
82
85
|
self.triggered_bottom_edge,
|
|
83
86
|
],
|
|
87
|
+
wrapper=HintedSignal,
|
|
84
88
|
)
|
|
85
89
|
|
|
86
90
|
super().__init__(name=name)
|
|
87
91
|
|
|
88
|
-
|
|
92
|
+
def _set_triggered_values(self, results: SampleLocation):
|
|
89
93
|
tip = (results.tip_x, results.tip_y)
|
|
90
94
|
if tip == self.INVALID_POSITION:
|
|
91
95
|
raise InvalidPinException
|
|
92
96
|
else:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
self._tip_setter(tip)
|
|
98
|
+
self._top_edge_setter(results.edge_top)
|
|
99
|
+
self._bottom_edge_setter(results.edge_bottom)
|
|
96
100
|
|
|
97
101
|
async def _get_tip_and_edge_data(
|
|
98
102
|
self, array_data: NDArray[np.uint8]
|
|
@@ -132,9 +136,7 @@ class PinTipDetection(StandardReadable):
|
|
|
132
136
|
location = sample_detection.processArray(array_data)
|
|
133
137
|
end_time = time.time()
|
|
134
138
|
LOGGER.debug(
|
|
135
|
-
"Sample location detection took {}ms"
|
|
136
|
-
(end_time - start_time) * 1000.0
|
|
137
|
-
)
|
|
139
|
+
f"Sample location detection took {(end_time - start_time) * 1000.0}ms"
|
|
138
140
|
)
|
|
139
141
|
return location
|
|
140
142
|
|
|
@@ -150,9 +152,9 @@ class PinTipDetection(StandardReadable):
|
|
|
150
152
|
async for value in observe_value(self.array_data):
|
|
151
153
|
try:
|
|
152
154
|
location = await self._get_tip_and_edge_data(value)
|
|
153
|
-
|
|
155
|
+
self._set_triggered_values(location)
|
|
154
156
|
except Exception as e:
|
|
155
|
-
LOGGER.
|
|
157
|
+
LOGGER.warning(
|
|
156
158
|
f"Failed to detect pin-tip location, will retry with next image: {e}"
|
|
157
159
|
)
|
|
158
160
|
else:
|
|
@@ -166,6 +168,6 @@ class PinTipDetection(StandardReadable):
|
|
|
166
168
|
LOGGER.error(
|
|
167
169
|
f"No tip found in {await self.validity_timeout.get_value()} seconds."
|
|
168
170
|
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
self._tip_setter(self.INVALID_POSITION)
|
|
172
|
+
self._bottom_edge_setter(np.array([]))
|
|
173
|
+
self._top_edge_setter(np.array([]))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from enum import Enum
|
|
3
|
-
from typing import
|
|
4
|
+
from typing import Final
|
|
4
5
|
|
|
5
6
|
import cv2
|
|
6
7
|
import numpy as np
|
|
@@ -103,7 +104,7 @@ class SampleLocation:
|
|
|
103
104
|
edge_bottom: np.ndarray
|
|
104
105
|
|
|
105
106
|
|
|
106
|
-
class MxSampleDetect
|
|
107
|
+
class MxSampleDetect:
|
|
107
108
|
def __init__(
|
|
108
109
|
self,
|
|
109
110
|
*,
|
|
@@ -161,7 +162,7 @@ class MxSampleDetect(object):
|
|
|
161
162
|
@staticmethod
|
|
162
163
|
def _first_and_last_nonzero_by_columns(
|
|
163
164
|
arr: np.ndarray,
|
|
164
|
-
) ->
|
|
165
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
165
166
|
"""
|
|
166
167
|
Finds the indexes of the first & last non-zero values by column in a 2d array.
|
|
167
168
|
|
|
@@ -243,9 +244,7 @@ class MxSampleDetect(object):
|
|
|
243
244
|
bottom[x + 1 :] = NONE_VALUE
|
|
244
245
|
|
|
245
246
|
LOGGER.info(
|
|
246
|
-
"pin-tip detection: Successfully located pin tip at (x={}, y={})"
|
|
247
|
-
tip_x, tip_y
|
|
248
|
-
)
|
|
247
|
+
f"pin-tip detection: Successfully located pin tip at (x={tip_x}, y={tip_y})"
|
|
249
248
|
)
|
|
250
249
|
return SampleLocation(
|
|
251
250
|
tip_x=tip_x, tip_y=tip_y, edge_bottom=bottom, edge_top=top
|