dls-dodal 1.42.0__py3-none-any.whl → 1.44.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.42.0.dist-info → dls_dodal-1.44.0.dist-info}/METADATA +5 -4
- {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/RECORD +62 -54
- {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/b01_1.py +8 -0
- dodal/beamlines/i03.py +11 -0
- dodal/beamlines/i13_1.py +22 -48
- dodal/beamlines/i19_1.py +16 -5
- dodal/beamlines/i19_2.py +12 -3
- dodal/beamlines/i19_optics.py +4 -2
- dodal/beamlines/training_rig.py +5 -1
- dodal/common/beamlines/beamline_utils.py +6 -9
- dodal/common/data_util.py +20 -0
- dodal/common/signal_utils.py +43 -4
- dodal/common/visit.py +41 -1
- dodal/devices/aperturescatterguard.py +4 -4
- dodal/devices/apple2_undulator.py +10 -8
- dodal/devices/attenuator/attenuator.py +1 -1
- dodal/devices/backlight.py +1 -1
- dodal/devices/baton.py +17 -0
- dodal/devices/bimorph_mirror.py +2 -2
- dodal/devices/current_amplifiers/current_amplifier.py +1 -6
- dodal/devices/current_amplifiers/current_amplifier_detector.py +2 -2
- dodal/devices/current_amplifiers/femto.py +0 -5
- dodal/devices/current_amplifiers/sr570.py +0 -5
- dodal/devices/electron_analyser/__init__.py +0 -0
- dodal/devices/electron_analyser/base_region.py +64 -0
- dodal/devices/electron_analyser/specs/__init__.py +0 -0
- dodal/devices/electron_analyser/specs/specs_region.py +24 -0
- dodal/devices/electron_analyser/vgscienta/__init__.py +0 -0
- dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +77 -0
- dodal/devices/fast_grid_scan.py +2 -2
- dodal/devices/hutch_shutter.py +1 -1
- dodal/devices/i03/beamstop.py +2 -2
- dodal/devices/i10/i10_apple2.py +3 -3
- dodal/devices/i13_1/merlin.py +1 -2
- dodal/devices/i13_1/merlin_controller.py +12 -8
- dodal/devices/i19/beamstop.py +30 -0
- dodal/devices/i19/hutch_access.py +2 -0
- dodal/devices/i19/shutter.py +52 -30
- dodal/devices/i22/nxsas.py +1 -3
- dodal/devices/i24/focus_mirrors.py +3 -3
- dodal/devices/i24/pilatus_metadata.py +2 -2
- dodal/devices/i24/pmac.py +2 -2
- dodal/devices/oav/oav_detector.py +8 -10
- dodal/devices/oav/snapshots/snapshot.py +21 -0
- dodal/devices/oav/snapshots/snapshot_image_processing.py +74 -0
- dodal/devices/pressure_jump_cell.py +7 -3
- dodal/devices/robot.py +1 -1
- dodal/devices/thawer.py +4 -4
- dodal/devices/undulator.py +1 -1
- dodal/devices/undulator_dcm.py +1 -1
- dodal/devices/util/epics_util.py +1 -1
- dodal/devices/zebra/zebra.py +4 -3
- dodal/devices/zebra/zebra_controlled_shutter.py +1 -1
- dodal/plan_stubs/data_session.py +17 -9
- dodal/plan_stubs/motor_utils.py +10 -12
- dodal/plan_stubs/wrapped.py +10 -12
- dodal/utils.py +0 -7
- dodal/devices/i13_1/merlin_io.py +0 -17
- dodal/devices/oav/microns_for_zoom_levels.json +0 -55
- dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +0 -64
- {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info/licenses}/LICENSE +0 -0
- {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/top_level.txt +0 -0
dodal/devices/i13_1/merlin.py
CHANGED
|
@@ -3,7 +3,6 @@ from ophyd_async.epics import adcore
|
|
|
3
3
|
|
|
4
4
|
from dodal.common.beamlines.device_helpers import CAM_SUFFIX, HDF5_SUFFIX
|
|
5
5
|
from dodal.devices.i13_1.merlin_controller import MerlinController
|
|
6
|
-
from dodal.devices.i13_1.merlin_io import MerlinDriverIO
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class Merlin(StandardDetector):
|
|
@@ -18,7 +17,7 @@ class Merlin(StandardDetector):
|
|
|
18
17
|
fileio_suffix=HDF5_SUFFIX,
|
|
19
18
|
name: str = "",
|
|
20
19
|
):
|
|
21
|
-
self.drv =
|
|
20
|
+
self.drv = adcore.ADBaseIO(prefix + drv_suffix)
|
|
22
21
|
self.hdf = adcore.NDFileHDFIO(prefix + fileio_suffix)
|
|
23
22
|
|
|
24
23
|
super().__init__(
|
|
@@ -6,17 +6,21 @@ from ophyd_async.core import (
|
|
|
6
6
|
AsyncStatus,
|
|
7
7
|
TriggerInfo,
|
|
8
8
|
)
|
|
9
|
-
from ophyd_async.epics import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
from ophyd_async.epics.adcore import (
|
|
10
|
+
DEFAULT_GOOD_STATES,
|
|
11
|
+
ADBaseController,
|
|
12
|
+
ADBaseIO,
|
|
13
|
+
ADImageMode,
|
|
14
|
+
ADState,
|
|
15
|
+
stop_busy_record,
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class MerlinController(ADBaseController):
|
|
16
20
|
def __init__(
|
|
17
21
|
self,
|
|
18
|
-
driver:
|
|
19
|
-
good_states: frozenset[
|
|
22
|
+
driver: ADBaseIO,
|
|
23
|
+
good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
|
|
20
24
|
) -> None:
|
|
21
25
|
self.driver = driver
|
|
22
26
|
self.good_states = good_states
|
|
@@ -34,7 +38,7 @@ class MerlinController(ADBaseController):
|
|
|
34
38
|
)
|
|
35
39
|
await asyncio.gather(
|
|
36
40
|
self.driver.num_images.set(trigger_info.total_number_of_triggers),
|
|
37
|
-
self.driver.image_mode.set(
|
|
41
|
+
self.driver.image_mode.set(ADImageMode.MULTIPLE),
|
|
38
42
|
)
|
|
39
43
|
|
|
40
44
|
async def wait_for_idle(self):
|
|
@@ -44,4 +48,4 @@ class MerlinController(ADBaseController):
|
|
|
44
48
|
async def disarm(self):
|
|
45
49
|
# We can't use caput callback as we already used it in arm() and we can't have
|
|
46
50
|
# 2 or they will deadlock
|
|
47
|
-
await
|
|
51
|
+
await stop_busy_record(self.driver.acquire, False, timeout=1)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable, StrictEnum
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_rw, epics_signal_x
|
|
3
|
+
from ophyd_async.epics.motor import Motor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HomeGroup(StrictEnum):
|
|
7
|
+
NONE = "none"
|
|
8
|
+
ALL = "All"
|
|
9
|
+
X = "X"
|
|
10
|
+
Y = "Y"
|
|
11
|
+
Z = "Z"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HomingControl(StandardReadable):
|
|
15
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
16
|
+
self.homing_group = epics_signal_rw(HomeGroup, f"{prefix}:HMGRP")
|
|
17
|
+
self.home = epics_signal_x(f"{prefix}:HOME")
|
|
18
|
+
super().__init__(name)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BeamStop(StandardReadable):
|
|
22
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
23
|
+
with self.add_children_as_readables():
|
|
24
|
+
self.x = Motor(f"{prefix}X")
|
|
25
|
+
self.y = Motor(f"{prefix}Y")
|
|
26
|
+
self.z = Motor(f"{prefix}Z")
|
|
27
|
+
|
|
28
|
+
self.homing = HomingControl(f"{prefix}HM", name)
|
|
29
|
+
|
|
30
|
+
super().__init__(name)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from ophyd_async.core import StandardReadable
|
|
2
2
|
from ophyd_async.epics.core import epics_signal_r
|
|
3
3
|
|
|
4
|
+
ACCESS_DEVICE_NAME = "access_control" # Device name in i19-blueapi
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
class HutchAccessControl(StandardReadable):
|
|
6
8
|
def __init__(self, prefix: str, name: str = "") -> None:
|
dodal/devices/i19/shutter.py
CHANGED
|
@@ -1,57 +1,79 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
|
+
from aiohttp import ClientSession
|
|
3
4
|
from bluesky.protocols import Movable
|
|
4
|
-
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
5
|
+
from ophyd_async.core import AsyncStatus, StandardReadable, StandardReadableFormat
|
|
5
6
|
from ophyd_async.epics.core import epics_signal_r
|
|
6
7
|
|
|
7
|
-
from dodal.devices.hutch_shutter import
|
|
8
|
+
from dodal.devices.hutch_shutter import ShutterDemand, ShutterState
|
|
9
|
+
from dodal.devices.i19.hutch_access import ACCESS_DEVICE_NAME
|
|
8
10
|
from dodal.log import LOGGER
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
class HutchInvalidError(Exception):
|
|
12
|
-
pass
|
|
12
|
+
OPTICS_BLUEAPI_URL = "https://i19-blueapi.diamond.ac.uk"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class HutchState(str, Enum):
|
|
16
16
|
EH1 = "EH1"
|
|
17
17
|
EH2 = "EH2"
|
|
18
|
-
INVALID = "INVALID"
|
|
19
18
|
|
|
20
19
|
|
|
21
|
-
class
|
|
20
|
+
class AccessControlledShutter(StandardReadable, Movable[ShutterDemand]):
|
|
22
21
|
""" I19-specific device to operate the hutch shutter.
|
|
23
22
|
|
|
24
|
-
This device
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
This device will send a REST call to the blueapi instance controlling the optics \
|
|
24
|
+
hutch running on the I19 cluster, which will evaluate the current hutch in use vs \
|
|
25
|
+
the hutch sending the request and decide if the plan will be run or not.
|
|
27
26
|
As the two hutches are located in series, checking the hutch in use is necessary to \
|
|
28
27
|
avoid accidentally operating the shutter from one hutch while the other has beamtime.
|
|
29
28
|
|
|
30
|
-
The
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
The name of the hutch that wants to operate the shutter should be passed to the \
|
|
30
|
+
device upon instantiation.
|
|
31
|
+
|
|
32
|
+
For details see the architecture described in \
|
|
33
|
+
https://github.com/DiamondLightSource/i19-bluesky/issues/30.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
def __init__(self, prefix: str, hutch: HutchState, name: str = "") -> None:
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
self.hutch_state = epics_signal_r(str, f"{bl_prefix}-OP-STAT-01:EHStatus.VALA")
|
|
37
|
+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
38
|
+
self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
|
|
40
39
|
self.hutch_request = hutch
|
|
40
|
+
self.url = OPTICS_BLUEAPI_URL
|
|
41
41
|
super().__init__(name)
|
|
42
42
|
|
|
43
43
|
@AsyncStatus.wrap
|
|
44
44
|
async def set(self, value: ShutterDemand):
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
45
|
+
REQUEST_PARAMS = {
|
|
46
|
+
"name": "operate_shutter_plan",
|
|
47
|
+
"params": {
|
|
48
|
+
"experiment_hutch": self.hutch_request.value,
|
|
49
|
+
"access_device": ACCESS_DEVICE_NAME,
|
|
50
|
+
"shutter_demand": value,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
async with ClientSession(base_url=self.url, raise_for_status=True) as session:
|
|
54
|
+
# First submit the plan to the worker
|
|
55
|
+
async with session.post("/tasks", data=REQUEST_PARAMS) as response:
|
|
56
|
+
LOGGER.debug(
|
|
57
|
+
f"Task submitted to the worker, response status: {response.status}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
data = await response.json()
|
|
62
|
+
task_id = data["task_id"]
|
|
63
|
+
except Exception as e:
|
|
64
|
+
LOGGER.error(
|
|
65
|
+
f"Failed to get task_id from {self.url}/tasks POST. ({e})"
|
|
66
|
+
)
|
|
67
|
+
raise
|
|
68
|
+
# Then set the task as active and run asap
|
|
69
|
+
async with session.put(
|
|
70
|
+
"/worker/tasks", data={"task_id": task_id}
|
|
71
|
+
) as response:
|
|
72
|
+
if not response.ok:
|
|
73
|
+
LOGGER.error(
|
|
74
|
+
f"""Unable to operate the shutter.
|
|
75
|
+
Session PUT responded with {response.status}: {response.reason}.
|
|
76
|
+
"""
|
|
77
|
+
)
|
|
78
|
+
return
|
|
79
|
+
LOGGER.debug(f"Run operate shutter plan, task_id: {task_id}")
|
dodal/devices/i22/nxsas.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import TypeVar
|
|
|
6
6
|
from bluesky.protocols import Reading
|
|
7
7
|
from event_model.documents.event_descriptor import DataKey
|
|
8
8
|
from ophyd_async.core import PathProvider
|
|
9
|
-
from ophyd_async.epics.adaravis import
|
|
9
|
+
from ophyd_async.epics.adaravis import AravisDetector
|
|
10
10
|
from ophyd_async.epics.adpilatus import PilatusDetector
|
|
11
11
|
|
|
12
12
|
ValueAndUnits = tuple[float, str]
|
|
@@ -149,7 +149,6 @@ class NXSasOAV(AravisDetector):
|
|
|
149
149
|
fileio_suffix: str,
|
|
150
150
|
metadata_holder: NXSasMetadataHolder,
|
|
151
151
|
name: str = "",
|
|
152
|
-
gpio_number: AravisController.GPIO_NUMBER = 1,
|
|
153
152
|
):
|
|
154
153
|
"""Extends detector with configuration metadata required or desired
|
|
155
154
|
to comply with the NXsas application definition.
|
|
@@ -162,7 +161,6 @@ class NXSasOAV(AravisDetector):
|
|
|
162
161
|
drv_suffix=drv_suffix,
|
|
163
162
|
fileio_suffix=fileio_suffix,
|
|
164
163
|
name=name,
|
|
165
|
-
gpio_number=gpio_number,
|
|
166
164
|
)
|
|
167
165
|
self._metadata_holder = metadata_holder
|
|
168
166
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from ophyd_async.core import StandardReadable, StrictEnum
|
|
2
2
|
from ophyd_async.epics.core import epics_signal_rw
|
|
3
3
|
|
|
4
|
-
from dodal.common.signal_utils import
|
|
4
|
+
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class HFocusMode(StrictEnum):
|
|
@@ -40,10 +40,10 @@ class FocusMirrorsMode(StandardReadable):
|
|
|
40
40
|
self.vertical = epics_signal_rw(VFocusMode, prefix + "G0:TARGETAPPLY")
|
|
41
41
|
|
|
42
42
|
with self.add_children_as_readables():
|
|
43
|
-
self.beam_size_x =
|
|
43
|
+
self.beam_size_x = create_r_hardware_backed_soft_signal(
|
|
44
44
|
int, self._get_beam_size_x, units="um"
|
|
45
45
|
)
|
|
46
|
-
self.beam_size_y =
|
|
46
|
+
self.beam_size_y = create_r_hardware_backed_soft_signal(
|
|
47
47
|
int, self._get_beam_size_y, units="um"
|
|
48
48
|
)
|
|
49
49
|
|
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
from ophyd_async.core import StandardReadable
|
|
6
6
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
7
7
|
|
|
8
|
-
from dodal.common.signal_utils import
|
|
8
|
+
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class PilatusMetadata(StandardReadable):
|
|
@@ -14,7 +14,7 @@ class PilatusMetadata(StandardReadable):
|
|
|
14
14
|
self.template = epics_signal_r(str, prefix + "cam1:FileTemplate_RBV")
|
|
15
15
|
self.filenumber = epics_signal_r(int, prefix + "cam1:FileNumber_RBV")
|
|
16
16
|
with self.add_children_as_readables():
|
|
17
|
-
self.filename_template =
|
|
17
|
+
self.filename_template = create_r_hardware_backed_soft_signal(
|
|
18
18
|
str, self._get_full_filename_template
|
|
19
19
|
)
|
|
20
20
|
super().__init__(name)
|
dodal/devices/i24/pmac.py
CHANGED
|
@@ -71,7 +71,7 @@ class PMACStringMove(Triggerable):
|
|
|
71
71
|
await self.signal_ref().set(self.cmd_string, wait=True)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
class PMACStringLaser(Device, Movable):
|
|
74
|
+
class PMACStringLaser(Device, Movable[LaserSettings]):
|
|
75
75
|
"""Set the pmac_string to control the laser."""
|
|
76
76
|
|
|
77
77
|
def __init__(
|
|
@@ -90,7 +90,7 @@ class PMACStringLaser(Device, Movable):
|
|
|
90
90
|
await self._signal_ref().set(value.value)
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
class PMACStringEncReset(Device, Movable):
|
|
93
|
+
class PMACStringEncReset(Device, Movable[EncReset]):
|
|
94
94
|
"""Set a pmac_string to control the encoder channels in the controller."""
|
|
95
95
|
|
|
96
96
|
def __init__(
|
|
@@ -9,10 +9,10 @@ from ophyd_async.core import (
|
|
|
9
9
|
)
|
|
10
10
|
from ophyd_async.epics.core import epics_signal_rw
|
|
11
11
|
|
|
12
|
-
from dodal.common.signal_utils import
|
|
12
|
+
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
13
13
|
from dodal.devices.areadetector.plugins.CAM import Cam
|
|
14
14
|
from dodal.devices.oav.oav_parameters import DEFAULT_OAV_WINDOW, OAVConfig
|
|
15
|
-
from dodal.devices.oav.snapshots.
|
|
15
|
+
from dodal.devices.oav.snapshots.snapshot import Snapshot
|
|
16
16
|
from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
|
|
17
17
|
|
|
18
18
|
|
|
@@ -29,7 +29,7 @@ def _get_correct_zoom_string(zoom: str) -> str:
|
|
|
29
29
|
return zoom
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class ZoomController(StandardReadable, Movable):
|
|
32
|
+
class ZoomController(StandardReadable, Movable[str]):
|
|
33
33
|
"""
|
|
34
34
|
Device to control the zoom level. This should be set like
|
|
35
35
|
o = OAV(name="oav")
|
|
@@ -63,24 +63,22 @@ class OAV(StandardReadable):
|
|
|
63
63
|
|
|
64
64
|
with self.add_children_as_readables():
|
|
65
65
|
self.grid_snapshot = SnapshotWithGrid(f"{prefix}MJPG:", name)
|
|
66
|
-
self.microns_per_pixel_x =
|
|
66
|
+
self.microns_per_pixel_x = create_r_hardware_backed_soft_signal(
|
|
67
67
|
float,
|
|
68
68
|
lambda: self._get_microns_per_pixel(Coords.X),
|
|
69
69
|
)
|
|
70
|
-
self.microns_per_pixel_y =
|
|
70
|
+
self.microns_per_pixel_y = create_r_hardware_backed_soft_signal(
|
|
71
71
|
float,
|
|
72
72
|
lambda: self._get_microns_per_pixel(Coords.Y),
|
|
73
73
|
)
|
|
74
|
-
self.beam_centre_i =
|
|
74
|
+
self.beam_centre_i = create_r_hardware_backed_soft_signal(
|
|
75
75
|
int, lambda: self._get_beam_position(Coords.X)
|
|
76
76
|
)
|
|
77
|
-
self.beam_centre_j =
|
|
77
|
+
self.beam_centre_j = create_r_hardware_backed_soft_signal(
|
|
78
78
|
int, lambda: self._get_beam_position(Coords.Y)
|
|
79
79
|
)
|
|
80
|
-
self.snapshot =
|
|
80
|
+
self.snapshot = Snapshot(
|
|
81
81
|
f"{self._prefix}MJPG:",
|
|
82
|
-
self.beam_centre_i,
|
|
83
|
-
self.beam_centre_j,
|
|
84
82
|
self._name,
|
|
85
83
|
)
|
|
86
84
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from PIL import Image
|
|
2
|
+
|
|
3
|
+
from dodal.devices.areadetector.plugins.MJPG import MJPG
|
|
4
|
+
|
|
5
|
+
CROSSHAIR_LENGTH_PX = 20
|
|
6
|
+
CROSSHAIR_OUTLINE_COLOUR = "Black"
|
|
7
|
+
CROSSHAIR_FILL_COLOUR = "White"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Snapshot(MJPG):
|
|
11
|
+
"""A child of MJPG which, when triggered, saves the image to disk."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
prefix: str,
|
|
16
|
+
name: str = "",
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(prefix, name)
|
|
19
|
+
|
|
20
|
+
async def post_processing(self, image: Image.Image):
|
|
21
|
+
await self._save_image(image)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from PIL import Image, ImageDraw
|
|
2
|
+
|
|
3
|
+
from dodal.devices.oav.utils import Pixel
|
|
4
|
+
|
|
5
|
+
CROSSHAIR_LENGTH_PX = 20
|
|
6
|
+
CROSSHAIR_OUTLINE_COLOUR = "Black"
|
|
7
|
+
CROSSHAIR_FILL_COLOUR = "White"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def draw_crosshair(image: Image.Image, beam_x: int, beam_y: int):
|
|
11
|
+
"""
|
|
12
|
+
Draw a crosshair at the beam centre coordinates specified.
|
|
13
|
+
Args:
|
|
14
|
+
image: The image to draw the crosshair onto. This is mutated.
|
|
15
|
+
beam_x: The x-coordinate of the crosshair (pixels)
|
|
16
|
+
beam_y: The y-coordinate of the crosshair (pixels)
|
|
17
|
+
"""
|
|
18
|
+
draw = ImageDraw.Draw(image)
|
|
19
|
+
OUTLINE_WIDTH = 1
|
|
20
|
+
HALF_LEN = CROSSHAIR_LENGTH_PX / 2
|
|
21
|
+
draw.rectangle(
|
|
22
|
+
[
|
|
23
|
+
beam_x - OUTLINE_WIDTH,
|
|
24
|
+
beam_y - HALF_LEN - OUTLINE_WIDTH,
|
|
25
|
+
beam_x + OUTLINE_WIDTH,
|
|
26
|
+
beam_y + HALF_LEN + OUTLINE_WIDTH,
|
|
27
|
+
],
|
|
28
|
+
fill=CROSSHAIR_OUTLINE_COLOUR,
|
|
29
|
+
)
|
|
30
|
+
draw.rectangle(
|
|
31
|
+
[
|
|
32
|
+
beam_x - HALF_LEN - OUTLINE_WIDTH,
|
|
33
|
+
beam_y - OUTLINE_WIDTH,
|
|
34
|
+
beam_x + HALF_LEN + OUTLINE_WIDTH,
|
|
35
|
+
beam_y + OUTLINE_WIDTH,
|
|
36
|
+
],
|
|
37
|
+
fill=CROSSHAIR_OUTLINE_COLOUR,
|
|
38
|
+
)
|
|
39
|
+
draw.line(
|
|
40
|
+
((beam_x, beam_y - HALF_LEN), (beam_x, beam_y + HALF_LEN)),
|
|
41
|
+
fill=CROSSHAIR_FILL_COLOUR,
|
|
42
|
+
)
|
|
43
|
+
draw.line(
|
|
44
|
+
((beam_x - HALF_LEN, beam_y), (beam_x + HALF_LEN, beam_y)),
|
|
45
|
+
fill=CROSSHAIR_FILL_COLOUR,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compute_beam_centre_pixel_xy_for_mm_position(
|
|
50
|
+
sample_pos_mm: tuple[float, float],
|
|
51
|
+
beam_pos_at_origin_px: Pixel,
|
|
52
|
+
microns_per_pixel: tuple[float, float],
|
|
53
|
+
) -> Pixel:
|
|
54
|
+
"""
|
|
55
|
+
Compute the location of the beam centre in pixels on a reference image.
|
|
56
|
+
Args:
|
|
57
|
+
sample_pos_mm: x, y location of the sample in mm relative to when the reference image
|
|
58
|
+
was taken.
|
|
59
|
+
beam_pos_at_origin_px: x, y position of the beam centre in the reference image (pixels)
|
|
60
|
+
microns_per_pixel: x, y scaling factor relating the sample position to the position in the image.
|
|
61
|
+
Returns:
|
|
62
|
+
x, y location of the beam centre (pixels)
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def centre(sample_pos, beam_pos, um_per_px) -> int:
|
|
67
|
+
return beam_pos + sample_pos * 1000 / um_per_px
|
|
68
|
+
|
|
69
|
+
return Pixel(
|
|
70
|
+
centre(sp, bp, mpp)
|
|
71
|
+
for sp, bp, mpp in zip(
|
|
72
|
+
sample_pos_mm, beam_pos_at_origin_px, microns_per_pixel, strict=True
|
|
73
|
+
)
|
|
74
|
+
)
|
|
@@ -76,7 +76,7 @@ class AllValvesControlState:
|
|
|
76
76
|
valve_6: FastValveControlRequest | None = None
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
class AllValvesControl(StandardReadable, Movable):
|
|
79
|
+
class AllValvesControl(StandardReadable, Movable[AllValvesControlState]):
|
|
80
80
|
"""
|
|
81
81
|
valves 2, 4, 7, 8 are not controlled by the IOC,
|
|
82
82
|
as they are under manual control.
|
|
@@ -151,7 +151,9 @@ class AllValvesControl(StandardReadable, Movable):
|
|
|
151
151
|
)
|
|
152
152
|
|
|
153
153
|
|
|
154
|
-
class ValveControl(
|
|
154
|
+
class ValveControl(
|
|
155
|
+
StandardReadable, Movable[ValveControlRequest | ValveOpenSeqRequest]
|
|
156
|
+
):
|
|
155
157
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
156
158
|
with self.add_children_as_readables():
|
|
157
159
|
self.close = epics_signal_rw(ValveControlRequest, prefix + ":CON")
|
|
@@ -170,7 +172,9 @@ class ValveControl(StandardReadable, Movable):
|
|
|
170
172
|
return set_status
|
|
171
173
|
|
|
172
174
|
|
|
173
|
-
class FastValveControl(
|
|
175
|
+
class FastValveControl(
|
|
176
|
+
StandardReadable, Movable[FastValveControlRequest | ValveOpenSeqRequest]
|
|
177
|
+
):
|
|
174
178
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
175
179
|
with self.add_children_as_readables():
|
|
176
180
|
self.close = epics_signal_rw(FastValveControlRequest, prefix + ":CON")
|
dodal/devices/robot.py
CHANGED
|
@@ -52,7 +52,7 @@ class ErrorStatus(Device):
|
|
|
52
52
|
raise RobotLoadFailed(int(error_code), error_string) from raise_from
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
class BartRobot(StandardReadable, Movable):
|
|
55
|
+
class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
56
56
|
"""The sample changing robot."""
|
|
57
57
|
|
|
58
58
|
# How long to wait for the robot if it is busy soaking/drying
|
dodal/devices/thawer.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from asyncio import Task, create_task, sleep
|
|
2
2
|
|
|
3
|
-
from bluesky.protocols import Stoppable
|
|
3
|
+
from bluesky.protocols import Movable, Stoppable
|
|
4
4
|
from ophyd_async.core import (
|
|
5
5
|
AsyncStatus,
|
|
6
6
|
Device,
|
|
@@ -21,18 +21,18 @@ class ThawerStates(StrictEnum):
|
|
|
21
21
|
ON = "On"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
class ThawingTimer(Device, Stoppable):
|
|
24
|
+
class ThawingTimer(Device, Stoppable, Movable[float]):
|
|
25
25
|
def __init__(self, control_signal: SignalRW[ThawerStates]) -> None:
|
|
26
26
|
self._control_signal_ref = Reference(control_signal)
|
|
27
27
|
self._thawing_task: Task | None = None
|
|
28
28
|
super().__init__("thaw_for_time_s")
|
|
29
29
|
|
|
30
30
|
@AsyncStatus.wrap
|
|
31
|
-
async def set(self,
|
|
31
|
+
async def set(self, value: float):
|
|
32
32
|
await self._control_signal_ref().set(ThawerStates.ON)
|
|
33
33
|
if self._thawing_task and not self._thawing_task.done():
|
|
34
34
|
raise ThawingException("Thawing task already in progress")
|
|
35
|
-
self._thawing_task = create_task(sleep(
|
|
35
|
+
self._thawing_task = create_task(sleep(value))
|
|
36
36
|
try:
|
|
37
37
|
await self._thawing_task
|
|
38
38
|
finally:
|
dodal/devices/undulator.py
CHANGED
|
@@ -46,7 +46,7 @@ def _get_closest_gap_for_energy(
|
|
|
46
46
|
return table[1][idx]
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
class Undulator(StandardReadable, Movable):
|
|
49
|
+
class Undulator(StandardReadable, Movable[float]):
|
|
50
50
|
"""
|
|
51
51
|
An Undulator-type insertion device, used to control photon emission at a given
|
|
52
52
|
beam energy.
|
dodal/devices/undulator_dcm.py
CHANGED
|
@@ -12,7 +12,7 @@ from .undulator import Undulator
|
|
|
12
12
|
ENERGY_TIMEOUT_S: float = 30.0
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class UndulatorDCM(StandardReadable, Movable):
|
|
15
|
+
class UndulatorDCM(StandardReadable, Movable[float]):
|
|
16
16
|
"""
|
|
17
17
|
Composite device to handle changing beamline energies, wraps the Undulator and the
|
|
18
18
|
DCM. The DCM has a motor which controls the beam energy, when it moves, the
|
dodal/devices/util/epics_util.py
CHANGED
|
@@ -114,7 +114,7 @@ def call_func(func: Callable[[], StatusBase]) -> StatusBase:
|
|
|
114
114
|
return func()
|
|
115
115
|
|
|
116
116
|
|
|
117
|
-
class SetWhenEnabled(OphydAsyncDevice, Movable):
|
|
117
|
+
class SetWhenEnabled(OphydAsyncDevice, Movable[int]):
|
|
118
118
|
"""A device that sets the proc field of a PV when it becomes enabled."""
|
|
119
119
|
|
|
120
120
|
def __init__(self, name: str = "", prefix: str = ""):
|
dodal/devices/zebra/zebra.py
CHANGED
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from functools import partialmethod
|
|
6
6
|
|
|
7
|
+
from bluesky.protocols import Movable
|
|
7
8
|
from ophyd_async.core import (
|
|
8
9
|
AsyncStatus,
|
|
9
10
|
DeviceVector,
|
|
@@ -74,7 +75,7 @@ class SoftInState(StrictEnum):
|
|
|
74
75
|
NO = "No"
|
|
75
76
|
|
|
76
77
|
|
|
77
|
-
class ArmingDevice(StandardReadable):
|
|
78
|
+
class ArmingDevice(StandardReadable, Movable[ArmDemand]):
|
|
78
79
|
"""A useful device that can abstract some of the logic of arming.
|
|
79
80
|
Allows a user to just call arm.set(ArmDemand.ARM)"""
|
|
80
81
|
|
|
@@ -94,8 +95,8 @@ class ArmingDevice(StandardReadable):
|
|
|
94
95
|
return
|
|
95
96
|
|
|
96
97
|
@AsyncStatus.wrap
|
|
97
|
-
async def set(self,
|
|
98
|
-
await asyncio.wait_for(self._set_armed(
|
|
98
|
+
async def set(self, value: ArmDemand):
|
|
99
|
+
await asyncio.wait_for(self._set_armed(value), timeout=self.TIMEOUT)
|
|
99
100
|
|
|
100
101
|
|
|
101
102
|
class PositionCompare(StandardReadable):
|
|
@@ -19,7 +19,7 @@ class ZebraShutterControl(StrictEnum):
|
|
|
19
19
|
AUTO = "Auto"
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class ZebraShutter(StandardReadable, Movable):
|
|
22
|
+
class ZebraShutter(StandardReadable, Movable[ZebraShutterState]):
|
|
23
23
|
"""The shutter on most MX beamlines is controlled by the zebra.
|
|
24
24
|
|
|
25
25
|
Internally in the zebra there are two AND gates, one for manual control and one for
|
dodal/plan_stubs/data_session.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from bluesky import plan_stubs as bps
|
|
2
4
|
from bluesky import preprocessors as bpp
|
|
3
5
|
from bluesky.utils import MsgGenerator, make_decorator
|
|
6
|
+
from ophyd_async.core import PathProvider
|
|
4
7
|
|
|
5
8
|
from dodal.common.beamlines.beamline_utils import get_path_provider
|
|
6
9
|
from dodal.common.types import UpdatingPathProvider
|
|
@@ -10,7 +13,7 @@ DATA_GROUPS = "data_groups"
|
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
def attach_data_session_metadata_wrapper(
|
|
13
|
-
plan: MsgGenerator, provider:
|
|
16
|
+
plan: MsgGenerator, provider: PathProvider | None = None
|
|
14
17
|
) -> MsgGenerator:
|
|
15
18
|
"""
|
|
16
19
|
Attach data session metadata to the runs within a plan and make it correlate
|
|
@@ -30,14 +33,19 @@ def attach_data_session_metadata_wrapper(
|
|
|
30
33
|
Yields:
|
|
31
34
|
Iterator[Msg]: Plan messages
|
|
32
35
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
provider = provider or get_path_provider()
|
|
37
|
+
if isinstance(provider, UpdatingPathProvider):
|
|
38
|
+
yield from bps.wait_for([provider.update])
|
|
39
|
+
ress = yield from bps.wait_for([provider.data_session])
|
|
40
|
+
data_session = ress[0].result()
|
|
41
|
+
# https://github.com/DiamondLightSource/dodal/issues/452
|
|
42
|
+
# As part of 452, write each dataCollection into their own folder, then can use resource_dir directly
|
|
43
|
+
yield from bpp.inject_md_wrapper(plan, md={DATA_SESSION: data_session})
|
|
44
|
+
else:
|
|
45
|
+
logging.warning(
|
|
46
|
+
f"{provider} is not an UpdatingPathProvider, {attach_data_session_metadata_wrapper.__name__} will have no effect"
|
|
47
|
+
)
|
|
48
|
+
yield from plan
|
|
41
49
|
|
|
42
50
|
|
|
43
51
|
attach_data_session_metadata_decorator = make_decorator(
|