dls-dodal 1.33.0__py3-none-any.whl → 1.35.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.33.0.dist-info → dls_dodal-1.35.0.dist-info}/METADATA +3 -3
- dls_dodal-1.35.0.dist-info/RECORD +147 -0
- {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/WHEEL +1 -1
- dodal/__init__.py +8 -0
- dodal/_version.py +2 -2
- dodal/beamline_specific_utils/i03.py +6 -2
- dodal/beamlines/__init__.py +2 -3
- dodal/beamlines/i03.py +41 -9
- dodal/beamlines/i04.py +26 -4
- dodal/beamlines/i10.py +257 -0
- dodal/beamlines/i22.py +25 -13
- dodal/beamlines/i24.py +11 -11
- dodal/beamlines/p38.py +24 -13
- dodal/common/beamlines/beamline_utils.py +1 -2
- dodal/common/crystal_metadata.py +61 -0
- dodal/common/signal_utils.py +10 -14
- dodal/common/types.py +2 -7
- dodal/devices/CTAB.py +1 -1
- dodal/devices/aperture.py +1 -1
- dodal/devices/aperturescatterguard.py +20 -8
- dodal/devices/apple2_undulator.py +603 -0
- dodal/devices/areadetector/plugins/CAM.py +29 -0
- dodal/devices/areadetector/plugins/MJPG.py +51 -106
- dodal/devices/attenuator.py +1 -1
- dodal/devices/backlight.py +11 -11
- dodal/devices/cryostream.py +3 -5
- dodal/devices/dcm.py +26 -2
- dodal/devices/detector/detector_motion.py +3 -5
- dodal/devices/diamond_filter.py +46 -0
- dodal/devices/eiger.py +6 -2
- dodal/devices/eiger_odin.py +48 -39
- dodal/devices/fast_grid_scan.py +1 -1
- dodal/devices/fluorescence_detector_motion.py +5 -7
- dodal/devices/focusing_mirror.py +26 -19
- dodal/devices/hutch_shutter.py +4 -5
- dodal/devices/i10/i10_apple2.py +399 -0
- dodal/devices/i10/i10_setting_data.py +7 -0
- dodal/devices/i22/dcm.py +50 -83
- dodal/devices/i22/fswitch.py +5 -5
- dodal/devices/i24/aperture.py +3 -5
- dodal/devices/i24/beamstop.py +3 -5
- dodal/devices/i24/dcm.py +1 -1
- dodal/devices/i24/dual_backlight.py +9 -11
- dodal/devices/i24/pmac.py +35 -46
- dodal/devices/i24/vgonio.py +16 -0
- dodal/devices/ipin.py +5 -3
- dodal/devices/linkam3.py +7 -7
- dodal/devices/oav/oav_calculations.py +22 -0
- dodal/devices/oav/oav_detector.py +118 -83
- dodal/devices/oav/oav_parameters.py +50 -104
- dodal/devices/oav/oav_to_redis_forwarder.py +77 -35
- dodal/devices/oav/pin_image_recognition/__init__.py +9 -7
- dodal/devices/oav/{grid_overlay.py → snapshots/grid_overlay.py} +16 -59
- dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +64 -0
- dodal/devices/oav/snapshots/snapshot_with_grid.py +57 -0
- dodal/devices/oav/utils.py +28 -27
- dodal/devices/p99/sample_stage.py +3 -5
- dodal/devices/pgm.py +40 -0
- dodal/devices/qbpm.py +18 -0
- dodal/devices/robot.py +5 -5
- dodal/devices/smargon.py +3 -3
- dodal/devices/synchrotron.py +9 -4
- dodal/devices/tetramm.py +9 -9
- dodal/devices/thawer.py +13 -7
- dodal/devices/undulator.py +7 -6
- dodal/devices/util/adjuster_plans.py +1 -1
- dodal/devices/util/epics_util.py +1 -1
- dodal/devices/util/lookup_tables.py +4 -5
- dodal/devices/watsonmarlow323_pump.py +45 -0
- dodal/devices/webcam.py +9 -2
- dodal/devices/xbpm_feedback.py +3 -5
- dodal/devices/xspress3/xspress3.py +8 -9
- dodal/devices/xspress3/xspress3_channel.py +3 -5
- dodal/devices/zebra.py +12 -8
- dodal/devices/zebra_controlled_shutter.py +5 -6
- dodal/devices/zocalo/__init__.py +2 -2
- dodal/devices/zocalo/zocalo_constants.py +3 -0
- dodal/devices/zocalo/zocalo_interaction.py +2 -1
- dodal/devices/zocalo/zocalo_results.py +105 -89
- dodal/plans/data_session_metadata.py +2 -2
- dodal/plans/motor_util_plans.py +11 -9
- dodal/utils.py +11 -0
- dls_dodal-1.33.0.dist-info/RECORD +0 -136
- dodal/beamlines/i04_1.py +0 -140
- dodal/devices/i24/i24_vgonio.py +0 -17
- dodal/devices/oav/oav_errors.py +0 -35
- {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
from
|
|
1
|
+
from ophyd_async.core import AsyncStatus, StandardReadable, StrictEnum
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
2
3
|
|
|
3
|
-
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
4
|
-
from ophyd_async.epics.signal import epics_signal_rw
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
class BacklightPositions(str, Enum):
|
|
5
|
+
class BacklightPositions(StrictEnum):
|
|
8
6
|
OUT = "Out"
|
|
9
7
|
IN = "In"
|
|
10
8
|
LOAD_CHECK = "LoadCheck"
|
|
@@ -12,7 +10,7 @@ class BacklightPositions(str, Enum):
|
|
|
12
10
|
DIODE = "Diode"
|
|
13
11
|
|
|
14
12
|
|
|
15
|
-
class LEDStatus(
|
|
13
|
+
class LEDStatus(StrictEnum):
|
|
16
14
|
OFF = "OFF"
|
|
17
15
|
ON = "ON"
|
|
18
16
|
|
|
@@ -26,8 +24,8 @@ class BacklightPositioner(StandardReadable):
|
|
|
26
24
|
super().__init__(name)
|
|
27
25
|
|
|
28
26
|
@AsyncStatus.wrap
|
|
29
|
-
async def set(self,
|
|
30
|
-
await self.pos_level.set(
|
|
27
|
+
async def set(self, value: BacklightPositions):
|
|
28
|
+
await self.pos_level.set(value, wait=True)
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
class DualBacklight(StandardReadable):
|
|
@@ -53,9 +51,9 @@ class DualBacklight(StandardReadable):
|
|
|
53
51
|
super().__init__(name)
|
|
54
52
|
|
|
55
53
|
@AsyncStatus.wrap
|
|
56
|
-
async def set(self,
|
|
57
|
-
await self.backlight_position.set(
|
|
58
|
-
if
|
|
54
|
+
async def set(self, value: BacklightPositions):
|
|
55
|
+
await self.backlight_position.set(value)
|
|
56
|
+
if value == BacklightPositions.OUT:
|
|
59
57
|
await self.backlight_state.set(LEDStatus.OFF, wait=True)
|
|
60
58
|
else:
|
|
61
59
|
await self.backlight_state.set(LEDStatus.ON, wait=True)
|
dodal/devices/i24/pmac.py
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
from asyncio import sleep
|
|
2
2
|
from enum import Enum, IntEnum
|
|
3
3
|
|
|
4
|
-
from bluesky.protocols import Flyable, Triggerable
|
|
4
|
+
from bluesky.protocols import Flyable, Movable, Triggerable
|
|
5
5
|
from ophyd_async.core import (
|
|
6
|
-
CALCULATE_TIMEOUT,
|
|
7
6
|
DEFAULT_TIMEOUT,
|
|
8
7
|
AsyncStatus,
|
|
9
|
-
|
|
8
|
+
Device,
|
|
9
|
+
Reference,
|
|
10
10
|
SignalR,
|
|
11
11
|
SignalRW,
|
|
12
|
-
SoftSignalBackend,
|
|
13
12
|
StandardReadable,
|
|
14
13
|
soft_signal_rw,
|
|
15
14
|
wait_for_value,
|
|
16
15
|
)
|
|
16
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
17
17
|
from ophyd_async.epics.motor import Motor
|
|
18
|
-
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
|
|
19
18
|
|
|
20
19
|
HOME_STR = r"\#1hmz\#2hmz\#3hmz" # Command to home the PMAC motors
|
|
21
20
|
ZERO_STR = "!x0y0z0" # Command to blend any ongoing move into new position
|
|
@@ -64,61 +63,53 @@ class PMACStringMove(Triggerable):
|
|
|
64
63
|
pmac_str_sig: SignalRW,
|
|
65
64
|
string_to_send: str,
|
|
66
65
|
) -> None:
|
|
67
|
-
self.
|
|
66
|
+
self.signal_ref = Reference(pmac_str_sig)
|
|
68
67
|
self.cmd_string = string_to_send
|
|
69
68
|
|
|
70
69
|
@AsyncStatus.wrap
|
|
71
70
|
async def trigger(self):
|
|
72
|
-
await self.
|
|
71
|
+
await self.signal_ref().set(self.cmd_string, wait=True)
|
|
73
72
|
|
|
74
73
|
|
|
75
|
-
class PMACStringLaser(
|
|
74
|
+
class PMACStringLaser(Device, Movable):
|
|
76
75
|
"""Set the pmac_string to control the laser."""
|
|
77
76
|
|
|
78
77
|
def __init__(
|
|
79
78
|
self,
|
|
80
79
|
pmac_str_sig: SignalRW,
|
|
81
|
-
backend: SignalBackend,
|
|
82
|
-
timeout: float | None = DEFAULT_TIMEOUT,
|
|
83
80
|
name: str = "",
|
|
84
81
|
) -> None:
|
|
85
|
-
self.
|
|
86
|
-
super().__init__(
|
|
82
|
+
self._signal_ref = Reference(pmac_str_sig)
|
|
83
|
+
super().__init__(name)
|
|
87
84
|
|
|
88
85
|
@AsyncStatus.wrap
|
|
89
86
|
async def set(
|
|
90
87
|
self,
|
|
91
88
|
value: LaserSettings,
|
|
92
|
-
wait=True,
|
|
93
|
-
timeout=CALCULATE_TIMEOUT,
|
|
94
89
|
):
|
|
95
|
-
await self.
|
|
90
|
+
await self._signal_ref().set(value.value)
|
|
96
91
|
|
|
97
92
|
|
|
98
|
-
class PMACStringEncReset(
|
|
93
|
+
class PMACStringEncReset(Device, Movable):
|
|
99
94
|
"""Set a pmac_string to control the encoder channels in the controller."""
|
|
100
95
|
|
|
101
96
|
def __init__(
|
|
102
97
|
self,
|
|
103
98
|
pmac_str_sig: SignalRW,
|
|
104
|
-
backend: SignalBackend,
|
|
105
|
-
timeout: float | None = DEFAULT_TIMEOUT,
|
|
106
99
|
name: str = "",
|
|
107
100
|
) -> None:
|
|
108
|
-
self.
|
|
109
|
-
super().__init__(
|
|
101
|
+
self._signal_ref = Reference(pmac_str_sig)
|
|
102
|
+
super().__init__(name)
|
|
110
103
|
|
|
111
104
|
@AsyncStatus.wrap
|
|
112
105
|
async def set(
|
|
113
106
|
self,
|
|
114
107
|
value: EncReset,
|
|
115
|
-
wait=True,
|
|
116
|
-
timeout=CALCULATE_TIMEOUT,
|
|
117
108
|
):
|
|
118
|
-
await self.
|
|
109
|
+
await self._signal_ref().set(value.value)
|
|
119
110
|
|
|
120
111
|
|
|
121
|
-
class ProgramRunner(
|
|
112
|
+
class ProgramRunner(Device, Flyable):
|
|
122
113
|
"""Run the collection by setting the program number on the PMAC string.
|
|
123
114
|
|
|
124
115
|
Once the program number has been set, wait for the collection to be complete.
|
|
@@ -131,21 +122,18 @@ class ProgramRunner(SignalRW, Flyable):
|
|
|
131
122
|
status_sig: SignalR,
|
|
132
123
|
prog_num_sig: SignalRW,
|
|
133
124
|
collection_time_sig: SignalRW,
|
|
134
|
-
backend: SignalBackend,
|
|
135
|
-
timeout: float | None = DEFAULT_TIMEOUT,
|
|
136
125
|
name: str = "",
|
|
137
126
|
) -> None:
|
|
138
|
-
self.
|
|
139
|
-
self.
|
|
140
|
-
self.
|
|
127
|
+
self._signal_ref = Reference(pmac_str_sig)
|
|
128
|
+
self._status_ref = Reference(status_sig)
|
|
129
|
+
self._prog_num_ref = Reference(prog_num_sig)
|
|
141
130
|
|
|
142
|
-
self.
|
|
143
|
-
self.KICKOFF_TIMEOUT = timeout
|
|
131
|
+
self._collection_time_ref = Reference(collection_time_sig)
|
|
144
132
|
|
|
145
|
-
super().__init__(
|
|
133
|
+
super().__init__(name)
|
|
146
134
|
|
|
147
135
|
async def _get_prog_number_string(self) -> str:
|
|
148
|
-
prog_num = await self.
|
|
136
|
+
prog_num = await self._prog_num_ref().get_value()
|
|
149
137
|
return f"&2b{prog_num}r"
|
|
150
138
|
|
|
151
139
|
@AsyncStatus.wrap
|
|
@@ -154,11 +142,11 @@ class ProgramRunner(SignalRW, Flyable):
|
|
|
154
142
|
wait for the scan status PV to go to 1.
|
|
155
143
|
"""
|
|
156
144
|
prog_num_str = await self._get_prog_number_string()
|
|
157
|
-
await self.
|
|
145
|
+
await self._signal_ref().set(prog_num_str, wait=True)
|
|
158
146
|
await wait_for_value(
|
|
159
|
-
self.
|
|
147
|
+
self._status_ref(),
|
|
160
148
|
ScanState.RUNNING,
|
|
161
|
-
timeout=
|
|
149
|
+
timeout=DEFAULT_TIMEOUT,
|
|
162
150
|
)
|
|
163
151
|
|
|
164
152
|
@AsyncStatus.wrap
|
|
@@ -169,8 +157,10 @@ class ProgramRunner(SignalRW, Flyable):
|
|
|
169
157
|
complete_time (float): total time required by the collection to \
|
|
170
158
|
finish correctly.
|
|
171
159
|
"""
|
|
172
|
-
scan_complete_time = await self.
|
|
173
|
-
await wait_for_value(
|
|
160
|
+
scan_complete_time = await self._collection_time_ref().get_value()
|
|
161
|
+
await wait_for_value(
|
|
162
|
+
self._status_ref(), ScanState.DONE, timeout=scan_complete_time
|
|
163
|
+
)
|
|
174
164
|
|
|
175
165
|
|
|
176
166
|
class ProgramAbort(Triggerable):
|
|
@@ -183,16 +173,16 @@ class ProgramAbort(Triggerable):
|
|
|
183
173
|
pmac_str_sig: SignalRW,
|
|
184
174
|
status_sig: SignalR,
|
|
185
175
|
) -> None:
|
|
186
|
-
self.
|
|
187
|
-
self.
|
|
176
|
+
self._signal_ref = Reference(pmac_str_sig)
|
|
177
|
+
self._status_ref = Reference(status_sig)
|
|
188
178
|
|
|
189
179
|
@AsyncStatus.wrap
|
|
190
180
|
async def trigger(self):
|
|
191
|
-
await self.
|
|
181
|
+
await self._signal_ref().set("A", wait=True)
|
|
192
182
|
await sleep(1.0) # TODO Check with scientist what this sleep is really for.
|
|
193
|
-
await self.
|
|
183
|
+
await self._signal_ref().set("P2401=0", wait=True)
|
|
194
184
|
await wait_for_value(
|
|
195
|
-
self.
|
|
185
|
+
self._status_ref(),
|
|
196
186
|
ScanState.DONE,
|
|
197
187
|
timeout=DEFAULT_TIMEOUT,
|
|
198
188
|
)
|
|
@@ -209,10 +199,10 @@ class PMAC(StandardReadable):
|
|
|
209
199
|
)
|
|
210
200
|
self.to_xyz_zero = PMACStringMove(self.pmac_string, ZERO_STR)
|
|
211
201
|
|
|
212
|
-
self.laser = PMACStringLaser(self.pmac_string
|
|
202
|
+
self.laser = PMACStringLaser(self.pmac_string)
|
|
213
203
|
|
|
214
204
|
self.enc_reset = PMACStringEncReset(
|
|
215
|
-
self.pmac_string,
|
|
205
|
+
self.pmac_string,
|
|
216
206
|
)
|
|
217
207
|
|
|
218
208
|
self.x = Motor(prefix + "X")
|
|
@@ -234,7 +224,6 @@ class PMAC(StandardReadable):
|
|
|
234
224
|
self.scanstatus,
|
|
235
225
|
self.program_number,
|
|
236
226
|
self.collection_time,
|
|
237
|
-
backend=SoftSignalBackend(str),
|
|
238
227
|
)
|
|
239
228
|
self.abort_program = ProgramAbort(self.pmac_string, self.scanstatus)
|
|
240
229
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable
|
|
2
|
+
from ophyd_async.epics.motor import Motor
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class VerticalGoniometer(StandardReadable):
|
|
6
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
7
|
+
self.x = Motor(prefix + "PINX")
|
|
8
|
+
self.z = Motor(prefix + "PINZ")
|
|
9
|
+
self.yh = Motor(prefix + "PINYH")
|
|
10
|
+
self.omega = Motor(prefix + "OMEGA")
|
|
11
|
+
|
|
12
|
+
self.real_x = Motor(prefix + "PINXS")
|
|
13
|
+
self.real_z = Motor(prefix + "PINZS")
|
|
14
|
+
self.fast_y = Motor(prefix + "PINYS")
|
|
15
|
+
|
|
16
|
+
super().__init__(name)
|
dodal/devices/ipin.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
from ophyd_async.core import
|
|
2
|
-
from ophyd_async.epics.
|
|
1
|
+
from ophyd_async.core import StandardReadable, StandardReadableFormat
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_r
|
|
3
3
|
|
|
4
4
|
|
|
5
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(
|
|
9
|
+
with self.add_children_as_readables(
|
|
10
|
+
format=StandardReadableFormat.HINTED_SIGNAL
|
|
11
|
+
):
|
|
10
12
|
self.pin_readback = epics_signal_r(float, prefix + "I")
|
|
11
13
|
super().__init__(name)
|
dodal/devices/linkam3.py
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
|
-
from enum import Enum
|
|
4
3
|
|
|
5
4
|
from bluesky.protocols import Location
|
|
6
5
|
from ophyd_async.core import (
|
|
7
|
-
ConfigSignal,
|
|
8
|
-
HintedSignal,
|
|
9
6
|
StandardReadable,
|
|
7
|
+
StandardReadableFormat,
|
|
8
|
+
StrictEnum,
|
|
10
9
|
WatchableAsyncStatus,
|
|
11
10
|
WatcherUpdate,
|
|
12
11
|
observe_value,
|
|
13
12
|
)
|
|
14
|
-
from ophyd_async.epics.
|
|
13
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
15
14
|
|
|
16
15
|
|
|
17
|
-
class PumpControl(
|
|
16
|
+
class PumpControl(StrictEnum):
|
|
18
17
|
Manual = "Manual"
|
|
19
18
|
Auto = "Auto"
|
|
20
19
|
|
|
@@ -62,9 +61,10 @@ class Linkam3(StandardReadable):
|
|
|
62
61
|
# status is a bitfield stored in a double?
|
|
63
62
|
self.status = epics_signal_r(float, prefix + "STATUS:")
|
|
64
63
|
|
|
65
|
-
self.add_readables((self.temp,),
|
|
64
|
+
self.add_readables((self.temp,), format=StandardReadableFormat.HINTED_SIGNAL)
|
|
66
65
|
self.add_readables(
|
|
67
|
-
(self.ramp_rate, self.speed, self.set_point),
|
|
66
|
+
(self.ramp_rate, self.speed, self.set_point),
|
|
67
|
+
format=StandardReadableFormat.CONFIG_SIGNAL,
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
super().__init__(name=name)
|
|
@@ -37,3 +37,25 @@ def camera_coordinates_to_xyz(
|
|
|
37
37
|
|
|
38
38
|
z = vertical * sine
|
|
39
39
|
return np.array([x, y, z], dtype=np.float64)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def calculate_beam_distance(
|
|
43
|
+
beam_centre: tuple[int, int],
|
|
44
|
+
horizontal_pixels: int,
|
|
45
|
+
vertical_pixels: int,
|
|
46
|
+
) -> tuple[int, int]:
|
|
47
|
+
"""
|
|
48
|
+
Calculates the distance between the beam centre and the given (horizontal, vertical).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
horizontal_pixels (int): The x (camera coordinates) value in pixels.
|
|
52
|
+
vertical_pixels (int): The y (camera coordinates) value in pixels.
|
|
53
|
+
Returns:
|
|
54
|
+
The distance between the beam centre and the (horizontal, vertical) point in pixels as a tuple
|
|
55
|
+
(horizontal_distance, vertical_distance).
|
|
56
|
+
"""
|
|
57
|
+
beam_x, beam_y = beam_centre
|
|
58
|
+
return (
|
|
59
|
+
beam_x - horizontal_pixels,
|
|
60
|
+
beam_y - vertical_pixels,
|
|
61
|
+
)
|
|
@@ -1,26 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import DEFAULT_TIMEOUT, AsyncStatus, LazyMock, StandardReadable
|
|
4
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
5
|
+
|
|
6
|
+
from dodal.common.signal_utils import create_hardware_backed_soft_signal
|
|
7
|
+
from dodal.devices.areadetector.plugins.CAM import Cam
|
|
8
|
+
from dodal.devices.oav.oav_parameters import DEFAULT_OAV_WINDOW, OAVConfig
|
|
9
|
+
from dodal.devices.oav.snapshots.snapshot_with_beam_centre import SnapshotWithBeamCentre
|
|
10
|
+
from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
|
|
11
|
+
from dodal.log import LOGGER
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ZoomLevelNotFoundError(Exception):
|
|
15
|
+
def __init__(self, errmsg):
|
|
16
|
+
LOGGER.error(errmsg)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Coords(IntEnum):
|
|
20
|
+
X = 0
|
|
21
|
+
Y = 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Workaround to deal with the fact that beamlines may have slightly different string
|
|
25
|
+
# descriptions of the zoom level"
|
|
26
|
+
def _get_correct_zoom_string(zoom: str) -> str:
|
|
27
|
+
if zoom.endswith("x"):
|
|
28
|
+
zoom = zoom.strip("x")
|
|
29
|
+
return zoom
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ZoomController(StandardReadable):
|
|
24
33
|
"""
|
|
25
34
|
Device to control the zoom level. This should be set like
|
|
26
35
|
o = OAV(name="oav")
|
|
@@ -30,63 +39,89 @@ class ZoomController(Device):
|
|
|
30
39
|
you should wait on any zoom changs to finish before changing the OAV wiring.
|
|
31
40
|
"""
|
|
32
41
|
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
42
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
43
|
+
super().__init__(name=name)
|
|
44
|
+
self.percentage = epics_signal_rw(float, f"{prefix}ZOOMPOSCMD")
|
|
45
|
+
|
|
46
|
+
# Level is the string description of the zoom level e.g. "1.0x" or "1.0"
|
|
47
|
+
self.level = epics_signal_rw(str, f"{prefix}MP:SELECT")
|
|
48
|
+
|
|
49
|
+
async def _get_allowed_zoom_levels(self) -> list:
|
|
50
|
+
zoom_levels = await self.level.describe()
|
|
51
|
+
return zoom_levels["level"]["choices"] # type: ignore
|
|
52
|
+
|
|
53
|
+
@AsyncStatus.wrap
|
|
54
|
+
async def set(self, level_to_set: str):
|
|
55
|
+
allowed_zoom_levels = await self._get_allowed_zoom_levels()
|
|
56
|
+
if level_to_set not in allowed_zoom_levels:
|
|
57
|
+
raise ZoomLevelNotFoundError(
|
|
58
|
+
f"{level_to_set} not found, expected one of {allowed_zoom_levels}"
|
|
59
|
+
)
|
|
60
|
+
await self.level.set(level_to_set, wait=True)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OAV(StandardReadable):
|
|
64
|
+
def __init__(self, prefix: str, config: OAVConfig, name: str = ""):
|
|
65
|
+
self.oav_config = config
|
|
66
|
+
self._prefix = prefix
|
|
67
|
+
self._name = name
|
|
68
|
+
_bl_prefix = prefix.split("-")[0]
|
|
69
|
+
self.zoom_controller = ZoomController(f"{_bl_prefix}-EA-OAV-01:FZOOM:", name)
|
|
70
|
+
|
|
71
|
+
self.cam = Cam(f"{prefix}CAM:", name=name)
|
|
72
|
+
|
|
73
|
+
with self.add_children_as_readables():
|
|
74
|
+
self.grid_snapshot = SnapshotWithGrid(f"{prefix}MJPG:", name)
|
|
75
|
+
self.microns_per_pixel_x = create_hardware_backed_soft_signal(
|
|
76
|
+
float,
|
|
77
|
+
lambda: self._get_microns_per_pixel(Coords.X),
|
|
78
|
+
)
|
|
79
|
+
self.microns_per_pixel_y = create_hardware_backed_soft_signal(
|
|
80
|
+
float,
|
|
81
|
+
lambda: self._get_microns_per_pixel(Coords.Y),
|
|
82
|
+
)
|
|
83
|
+
self.beam_centre_i = create_hardware_backed_soft_signal(
|
|
84
|
+
int, lambda: self._get_beam_position(Coords.X)
|
|
85
|
+
)
|
|
86
|
+
self.beam_centre_j = create_hardware_backed_soft_signal(
|
|
87
|
+
int, lambda: self._get_beam_position(Coords.Y)
|
|
88
|
+
)
|
|
89
|
+
self.snapshot = SnapshotWithBeamCentre(
|
|
90
|
+
f"{self._prefix}MJPG:",
|
|
91
|
+
self.beam_centre_i,
|
|
92
|
+
self.beam_centre_j,
|
|
93
|
+
self._name,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
self.sizes = [self.grid_snapshot.x_size, self.grid_snapshot.y_size]
|
|
97
|
+
|
|
98
|
+
super().__init__(name)
|
|
99
|
+
|
|
100
|
+
async def _read_current_zoom(self) -> str:
|
|
101
|
+
_zoom = await self.zoom_controller.level.get_value()
|
|
102
|
+
return _get_correct_zoom_string(_zoom)
|
|
103
|
+
|
|
104
|
+
async def _get_microns_per_pixel(self, coord: int) -> float:
|
|
105
|
+
"""Extracts the microns per x pixel and y pixel for a given zoom level."""
|
|
106
|
+
_zoom = await self._read_current_zoom()
|
|
107
|
+
value = self.parameters[_zoom].microns_per_pixel[coord]
|
|
108
|
+
size = await self.sizes[coord].get_value()
|
|
109
|
+
return value * DEFAULT_OAV_WINDOW[coord] / size
|
|
110
|
+
|
|
111
|
+
async def _get_beam_position(self, coord: int) -> int:
|
|
112
|
+
"""Extracts the beam location in pixels `xCentre` `yCentre`, for a requested \
|
|
113
|
+
zoom level. """
|
|
114
|
+
_zoom = await self._read_current_zoom()
|
|
115
|
+
value = self.parameters[_zoom].crosshair[coord]
|
|
116
|
+
size = await self.sizes[coord].get_value()
|
|
117
|
+
return int(value * size / DEFAULT_OAV_WINDOW[coord])
|
|
118
|
+
|
|
119
|
+
async def connect(
|
|
120
|
+
self,
|
|
121
|
+
mock: bool | LazyMock = False,
|
|
122
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
123
|
+
force_reconnect: bool = False,
|
|
124
|
+
):
|
|
125
|
+
self.parameters = self.oav_config.get_parameters()
|
|
126
|
+
|
|
127
|
+
return await super().connect(mock, timeout, force_reconnect)
|