dls-dodal 1.32.0__py3-none-any.whl → 1.34.1__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.32.0.dist-info → dls_dodal-1.34.1.dist-info}/METADATA +3 -3
- {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/RECORD +53 -43
- {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.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/b01_1.py +77 -0
- dodal/beamlines/i03.py +41 -9
- dodal/beamlines/i04.py +26 -4
- dodal/beamlines/i10.py +257 -0
- dodal/beamlines/i22.py +1 -2
- dodal/beamlines/i24.py +7 -7
- dodal/beamlines/p38.py +1 -2
- dodal/common/signal_utils.py +53 -0
- dodal/common/types.py +2 -7
- dodal/devices/aperturescatterguard.py +12 -15
- dodal/devices/apple2_undulator.py +602 -0
- dodal/devices/areadetector/plugins/CAM.py +31 -0
- dodal/devices/areadetector/plugins/MJPG.py +51 -106
- dodal/devices/backlight.py +7 -6
- dodal/devices/diamond_filter.py +47 -0
- dodal/devices/eiger.py +6 -2
- dodal/devices/eiger_odin.py +48 -39
- dodal/devices/focusing_mirror.py +14 -8
- dodal/devices/i10/i10_apple2.py +398 -0
- dodal/devices/i10/i10_setting_data.py +7 -0
- dodal/devices/i22/dcm.py +7 -8
- dodal/devices/i24/dual_backlight.py +5 -5
- dodal/devices/oav/oav_calculations.py +22 -0
- dodal/devices/oav/oav_detector.py +118 -97
- dodal/devices/oav/oav_parameters.py +50 -104
- dodal/devices/oav/oav_to_redis_forwarder.py +75 -34
- dodal/devices/oav/{grid_overlay.py → snapshots/grid_overlay.py} +0 -43
- 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 +26 -25
- dodal/devices/pgm.py +41 -0
- dodal/devices/qbpm.py +18 -0
- dodal/devices/robot.py +2 -2
- dodal/devices/smargon.py +2 -2
- dodal/devices/tetramm.py +2 -2
- dodal/devices/undulator.py +2 -1
- dodal/devices/util/adjuster_plans.py +1 -1
- dodal/devices/util/lookup_tables.py +4 -5
- dodal/devices/zebra.py +5 -2
- dodal/devices/zocalo/zocalo_results.py +13 -10
- dodal/plans/data_session_metadata.py +2 -2
- dodal/plans/motor_util_plans.py +11 -9
- dodal/utils.py +7 -0
- dodal/beamlines/i04_1.py +0 -140
- dodal/devices/oav/oav_errors.py +0 -35
- {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/LICENSE +0 -0
- {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/top_level.txt +0 -0
|
@@ -1,138 +1,83 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import threading
|
|
3
1
|
from abc import ABC, abstractmethod
|
|
4
2
|
from io import BytesIO
|
|
5
3
|
from pathlib import Path
|
|
6
4
|
|
|
7
|
-
import
|
|
8
|
-
from
|
|
9
|
-
from
|
|
5
|
+
import aiofiles
|
|
6
|
+
from aiohttp import ClientSession
|
|
7
|
+
from bluesky.protocols import Triggerable
|
|
8
|
+
from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_rw
|
|
9
|
+
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
|
|
10
|
+
from PIL import Image
|
|
10
11
|
|
|
11
|
-
from dodal.devices.oav.oav_parameters import OAVConfigParams
|
|
12
12
|
from dodal.log import LOGGER
|
|
13
13
|
|
|
14
|
+
IMG_FORMAT = "png"
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
async def asyncio_save_image(image: Image.Image, path: str):
|
|
18
|
+
buffer = BytesIO()
|
|
19
|
+
image.save(buffer, format=IMG_FORMAT)
|
|
20
|
+
async with aiofiles.open(path, "wb") as fh:
|
|
21
|
+
await fh.write(buffer.getbuffer())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MJPG(StandardReadable, Triggerable, ABC):
|
|
16
25
|
"""The MJPG areadetector plugin creates an MJPG video stream of the camera's output.
|
|
17
26
|
|
|
18
27
|
This devices uses that stream to grab images. When it is triggered it will send the
|
|
19
28
|
latest image from the stream to the `post_processing` method for child classes to handle.
|
|
20
29
|
"""
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
last_saved_path = Component(Signal)
|
|
25
|
-
url = Component(EpicsSignal, "JPG_URL_RBV", string=True)
|
|
26
|
-
x_size = Component(EpicsSignalRO, "ArraySize1_RBV")
|
|
27
|
-
y_size = Component(EpicsSignalRO, "ArraySize2_RBV")
|
|
28
|
-
input_rbpv = Component(EpicsSignalRO, "NDArrayPort_RBV")
|
|
29
|
-
input_plugin = Component(EpicsSignal, "NDArrayPort")
|
|
31
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
32
|
+
self.url = epics_signal_rw(str, prefix + "JPG_URL_RBV")
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
microns_per_pixel_y = Component(Signal)
|
|
34
|
+
self.x_size = epics_signal_r(int, prefix + "ArraySize1_RBV")
|
|
35
|
+
self.y_size = epics_signal_r(int, prefix + "ArraySize2_RBV")
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
with self.add_children_as_readables():
|
|
38
|
+
self.filename = soft_signal_rw(str)
|
|
39
|
+
self.directory = soft_signal_rw(str)
|
|
40
|
+
self.last_saved_path = soft_signal_rw(str)
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
self.KICKOFF_TIMEOUT = 30.0
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
super().__init__(name)
|
|
45
|
+
|
|
46
|
+
async def _save_image(self, image: Image.Image):
|
|
47
|
+
"""A helper function to save a given image to the path supplied by the \
|
|
48
|
+
directory and filename signals. The full resultant path is put on the \
|
|
49
|
+
last_saved_path signal
|
|
42
50
|
"""
|
|
43
|
-
filename_str = self.filename.
|
|
44
|
-
directory_str
|
|
51
|
+
filename_str = await self.filename.get_value()
|
|
52
|
+
directory_str = await self.directory.get_value()
|
|
45
53
|
|
|
46
|
-
path = Path(f"{directory_str}/{filename_str}.
|
|
47
|
-
if not
|
|
54
|
+
path = Path(f"{directory_str}/{filename_str}.{IMG_FORMAT}").as_posix()
|
|
55
|
+
if not Path(directory_str).is_dir():
|
|
48
56
|
LOGGER.info(f"Snapshot folder {directory_str} does not exist, creating...")
|
|
49
|
-
|
|
57
|
+
Path(directory_str).mkdir(parents=True)
|
|
50
58
|
|
|
51
59
|
LOGGER.info(f"Saving image to {path}")
|
|
52
|
-
image.save(path)
|
|
53
|
-
self.last_saved_path.put(path)
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
await asyncio_save_image(image, path)
|
|
62
|
+
|
|
63
|
+
await self.last_saved_path.set(path, wait=True)
|
|
64
|
+
|
|
65
|
+
@AsyncStatus.wrap
|
|
66
|
+
async def trigger(self):
|
|
56
67
|
"""This takes a snapshot image from the MJPG stream and send it to the
|
|
57
68
|
post_processing method, expected to be implemented by a child of this class.
|
|
58
69
|
|
|
59
|
-
It is the responsibility of the child class to save any resulting images
|
|
70
|
+
It is the responsibility of the child class to save any resulting images by \
|
|
71
|
+
calling _save_image.
|
|
60
72
|
"""
|
|
61
|
-
|
|
62
|
-
url_str = self.url.get()
|
|
73
|
+
url_str = await self.url.get_value()
|
|
63
74
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def get_snapshot():
|
|
71
|
-
try:
|
|
72
|
-
response = requests.get(url_str, stream=True)
|
|
73
|
-
response.raise_for_status()
|
|
74
|
-
with Image.open(BytesIO(response.content)) as image:
|
|
75
|
-
self.post_processing(image)
|
|
76
|
-
st.set_finished()
|
|
77
|
-
except requests.HTTPError as e:
|
|
78
|
-
st.set_exception(e)
|
|
79
|
-
|
|
80
|
-
threading.Thread(target=get_snapshot, daemon=True).start()
|
|
81
|
-
|
|
82
|
-
return st
|
|
75
|
+
async with ClientSession(raise_for_status=True) as session:
|
|
76
|
+
async with session.get(url_str) as response:
|
|
77
|
+
data = await response.read()
|
|
78
|
+
with Image.open(BytesIO(data)) as image:
|
|
79
|
+
await self.post_processing(image)
|
|
83
80
|
|
|
84
81
|
@abstractmethod
|
|
85
|
-
def post_processing(self, image: Image.Image):
|
|
82
|
+
async def post_processing(self, image: Image.Image):
|
|
86
83
|
pass
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class SnapshotWithBeamCentre(MJPG):
|
|
90
|
-
"""A child of MJPG which, when triggered, draws an outlined crosshair at the beam
|
|
91
|
-
centre in the image and saves the image to disk."""
|
|
92
|
-
|
|
93
|
-
CROSSHAIR_LENGTH_PX = 20
|
|
94
|
-
CROSSHAIR_OUTLINE_COLOUR = "Black"
|
|
95
|
-
CROSSHAIR_FILL_COLOUR = "White"
|
|
96
|
-
|
|
97
|
-
def post_processing(self, image: Image.Image):
|
|
98
|
-
assert (
|
|
99
|
-
self.oav_params is not None
|
|
100
|
-
), "Snapshot device does not have valid OAV parameters"
|
|
101
|
-
beam_x = self.oav_params.beam_centre_i
|
|
102
|
-
beam_y = self.oav_params.beam_centre_j
|
|
103
|
-
|
|
104
|
-
SnapshotWithBeamCentre.draw_crosshair(image, beam_x, beam_y)
|
|
105
|
-
|
|
106
|
-
self._save_image(image)
|
|
107
|
-
|
|
108
|
-
@classmethod
|
|
109
|
-
def draw_crosshair(cls, image: Image.Image, beam_x: int, beam_y: int):
|
|
110
|
-
draw = ImageDraw.Draw(image)
|
|
111
|
-
OUTLINE_WIDTH = 1
|
|
112
|
-
HALF_LEN = cls.CROSSHAIR_LENGTH_PX / 2
|
|
113
|
-
draw.rectangle(
|
|
114
|
-
[
|
|
115
|
-
beam_x - OUTLINE_WIDTH,
|
|
116
|
-
beam_y - HALF_LEN - OUTLINE_WIDTH,
|
|
117
|
-
beam_x + OUTLINE_WIDTH,
|
|
118
|
-
beam_y + HALF_LEN + OUTLINE_WIDTH,
|
|
119
|
-
],
|
|
120
|
-
fill=cls.CROSSHAIR_OUTLINE_COLOUR,
|
|
121
|
-
)
|
|
122
|
-
draw.rectangle(
|
|
123
|
-
[
|
|
124
|
-
beam_x - HALF_LEN - OUTLINE_WIDTH,
|
|
125
|
-
beam_y - OUTLINE_WIDTH,
|
|
126
|
-
beam_x + HALF_LEN + OUTLINE_WIDTH,
|
|
127
|
-
beam_y + OUTLINE_WIDTH,
|
|
128
|
-
],
|
|
129
|
-
fill=cls.CROSSHAIR_OUTLINE_COLOUR,
|
|
130
|
-
)
|
|
131
|
-
draw.line(
|
|
132
|
-
((beam_x, beam_y - HALF_LEN), (beam_x, beam_y + HALF_LEN)),
|
|
133
|
-
fill=cls.CROSSHAIR_FILL_COLOUR,
|
|
134
|
-
)
|
|
135
|
-
draw.line(
|
|
136
|
-
((beam_x - HALF_LEN, beam_y), (beam_x + HALF_LEN, beam_y)),
|
|
137
|
-
fill=cls.CROSSHAIR_FILL_COLOUR,
|
|
138
|
-
)
|
dodal/devices/backlight.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from asyncio import sleep
|
|
2
2
|
from enum import Enum
|
|
3
3
|
|
|
4
|
+
from bluesky.protocols import Movable
|
|
4
5
|
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
5
6
|
from ophyd_async.epics.signal import epics_signal_rw
|
|
6
7
|
|
|
@@ -15,10 +16,10 @@ class BacklightPosition(str, Enum):
|
|
|
15
16
|
OUT = "Out"
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class Backlight(StandardReadable):
|
|
19
|
+
class Backlight(StandardReadable, Movable):
|
|
19
20
|
"""Simple device to trigger the pneumatic in/out."""
|
|
20
21
|
|
|
21
|
-
TIME_TO_MOVE_S = 1 # Tested using a stopwatch on the beamline 09/2024
|
|
22
|
+
TIME_TO_MOVE_S = 1.0 # Tested using a stopwatch on the beamline 09/2024
|
|
22
23
|
|
|
23
24
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
24
25
|
with self.add_children_as_readables():
|
|
@@ -29,7 +30,7 @@ class Backlight(StandardReadable):
|
|
|
29
30
|
super().__init__(name)
|
|
30
31
|
|
|
31
32
|
@AsyncStatus.wrap
|
|
32
|
-
async def set(self,
|
|
33
|
+
async def set(self, value: BacklightPosition):
|
|
33
34
|
"""This setter will turn the backlight on when we move it in to the beam and off
|
|
34
35
|
when we move it out.
|
|
35
36
|
|
|
@@ -38,10 +39,10 @@ class Backlight(StandardReadable):
|
|
|
38
39
|
to move completely in/out so we sleep here to simulate this.
|
|
39
40
|
"""
|
|
40
41
|
old_position = await self.position.get_value()
|
|
41
|
-
await self.position.set(
|
|
42
|
-
if
|
|
42
|
+
await self.position.set(value)
|
|
43
|
+
if value == BacklightPosition.OUT:
|
|
43
44
|
await self.power.set(BacklightPower.OFF)
|
|
44
45
|
else:
|
|
45
46
|
await self.power.set(BacklightPower.ON)
|
|
46
|
-
if old_position !=
|
|
47
|
+
if old_position != value:
|
|
47
48
|
await sleep(self.TIME_TO_MOVE_S)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Generic, TypeVar
|
|
3
|
+
|
|
4
|
+
from ophyd_async.core import StandardReadable
|
|
5
|
+
from ophyd_async.epics.motor import Motor
|
|
6
|
+
from ophyd_async.epics.signal import epics_signal_rw
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _Filters(str, Enum):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class I03Filters(_Filters):
|
|
14
|
+
EMPTY = "Empty"
|
|
15
|
+
TWO_HUNDRED = "200um"
|
|
16
|
+
ONE_HUNDRED = "100um"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class I04Filters(_Filters):
|
|
20
|
+
EMPTY = "Empty"
|
|
21
|
+
TWO_HUNDRED = "200um"
|
|
22
|
+
FIFTY = "50um"
|
|
23
|
+
OUT = "Out"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T", bound=_Filters)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DiamondFilter(StandardReadable, Generic[T]):
|
|
30
|
+
"""
|
|
31
|
+
A filter set that is used to reduce the heat load on the monochromator.
|
|
32
|
+
|
|
33
|
+
It has 4 slots that can contain filters of different thickness. Changing the thickness
|
|
34
|
+
signal will move the filter set to select this filter.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
prefix: str,
|
|
40
|
+
data_type: type[T],
|
|
41
|
+
name: str = "",
|
|
42
|
+
) -> None:
|
|
43
|
+
with self.add_children_as_readables():
|
|
44
|
+
self.y_motor = Motor(prefix + "Y")
|
|
45
|
+
self.thickness = epics_signal_rw(data_type, f"{prefix}Y:MP:SELECT")
|
|
46
|
+
|
|
47
|
+
super().__init__(name)
|
dodal/devices/eiger.py
CHANGED
|
@@ -81,7 +81,9 @@ class EigerDetector(Device):
|
|
|
81
81
|
|
|
82
82
|
def async_stage(self):
|
|
83
83
|
self.odin.nodes.clear_odin_errors()
|
|
84
|
-
status_ok, error_message = self.odin.
|
|
84
|
+
status_ok, error_message = self.odin.wait_for_odin_initialised(
|
|
85
|
+
self.GENERAL_STATUS_TIMEOUT
|
|
86
|
+
)
|
|
85
87
|
if not status_ok:
|
|
86
88
|
raise Exception(f"Odin not initialised: {error_message}")
|
|
87
89
|
|
|
@@ -129,7 +131,9 @@ class EigerDetector(Device):
|
|
|
129
131
|
LOGGER.info("Disarming detector")
|
|
130
132
|
finally:
|
|
131
133
|
self.disarm_detector()
|
|
132
|
-
status_ok = self.odin.
|
|
134
|
+
status_ok = self.odin.check_and_wait_for_odin_state(
|
|
135
|
+
self.GENERAL_STATUS_TIMEOUT
|
|
136
|
+
)
|
|
133
137
|
self.disable_roi_mode()
|
|
134
138
|
return status_ok
|
|
135
139
|
|
dodal/devices/eiger_odin.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# type: ignore # Eiger will soon be ophyd-async https://github.com/DiamondLightSource/dodal/issues/700
|
|
2
|
+
from functools import partial, reduce
|
|
3
|
+
|
|
2
4
|
from ophyd import Component, Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
|
|
3
5
|
from ophyd.areadetector.plugins import HDF5Plugin_V22
|
|
4
6
|
from ophyd.sim import NullStatus
|
|
5
|
-
from ophyd.status import StatusBase
|
|
7
|
+
from ophyd.status import StatusBase, SubscriptionStatus
|
|
6
8
|
|
|
7
9
|
from dodal.devices.status import await_value
|
|
8
10
|
|
|
@@ -61,47 +63,47 @@ class OdinNodesStatus(Device):
|
|
|
61
63
|
def nodes(self) -> list[OdinNode]:
|
|
62
64
|
return [self.node_0, self.node_1, self.node_2, self.node_3]
|
|
63
65
|
|
|
64
|
-
def
|
|
66
|
+
def _check_node_frames_from_attr(
|
|
65
67
|
self, node_get_func, error_message_verb: str
|
|
66
68
|
) -> tuple[bool, str]:
|
|
67
69
|
nodes_frames_values = [0] * len(self.nodes)
|
|
68
70
|
frames_details = []
|
|
69
71
|
for node_number, node_pv in enumerate(self.nodes):
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
node_state = node_get_func(node_pv)
|
|
73
|
+
nodes_frames_values[node_number] = node_state
|
|
74
|
+
if node_state != 0:
|
|
75
|
+
error_message = f"Filewriter {node_number} {error_message_verb} \
|
|
76
|
+
{nodes_frames_values[node_number]} frames"
|
|
77
|
+
frames_details.append(error_message)
|
|
74
78
|
bad_frames = any(v != 0 for v in nodes_frames_values)
|
|
75
79
|
return bad_frames, "\n".join(frames_details)
|
|
76
80
|
|
|
77
81
|
def check_frames_timed_out(self) -> tuple[bool, str]:
|
|
78
|
-
return self.
|
|
82
|
+
return self._check_node_frames_from_attr(
|
|
79
83
|
lambda node: node.frames_timed_out.get(), "timed out"
|
|
80
84
|
)
|
|
81
85
|
|
|
82
86
|
def check_frames_dropped(self) -> tuple[bool, str]:
|
|
83
|
-
return self.
|
|
87
|
+
return self._check_node_frames_from_attr(
|
|
84
88
|
lambda node: node.frames_dropped.get(), "dropped"
|
|
85
89
|
)
|
|
86
90
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
error_messages = []
|
|
91
|
+
def wait_for_no_errors(self, timeout) -> dict[SubscriptionStatus, str]:
|
|
92
|
+
errors = {}
|
|
90
93
|
for node_number, node_pv in enumerate(self.nodes):
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
f"Filewriter {node_number} is in an error state with error message\
|
|
94
|
+
errors[
|
|
95
|
+
await_value(node_pv.error_status, False, timeout)
|
|
96
|
+
] = f"Filewriter {node_number} is in an error state with error message\
|
|
95
97
|
- {node_pv.error_message.get()}"
|
|
96
|
-
)
|
|
97
|
-
return any(is_error), "\n".join(error_messages)
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
return errors
|
|
100
|
+
|
|
101
|
+
def get_init_state(self, timeout) -> SubscriptionStatus:
|
|
100
102
|
is_initialised = []
|
|
101
103
|
for node_pv in self.nodes:
|
|
102
|
-
is_initialised.append(node_pv.fr_initialised
|
|
103
|
-
is_initialised.append(node_pv.fp_initialised
|
|
104
|
-
return
|
|
104
|
+
is_initialised.append(await_value(node_pv.fr_initialised, True, timeout))
|
|
105
|
+
is_initialised.append(await_value(node_pv.fp_initialised, True, timeout))
|
|
106
|
+
return reduce(lambda x, y: x & y, is_initialised)
|
|
105
107
|
|
|
106
108
|
def clear_odin_errors(self):
|
|
107
109
|
clearing_status = NullStatus()
|
|
@@ -125,8 +127,8 @@ class EigerOdin(Device):
|
|
|
125
127
|
writing_finished &= await_value(node_pv.writing, 0)
|
|
126
128
|
return writing_finished
|
|
127
129
|
|
|
128
|
-
def
|
|
129
|
-
is_initialised, error_message = self.
|
|
130
|
+
def check_and_wait_for_odin_state(self, timeout) -> bool:
|
|
131
|
+
is_initialised, error_message = self.wait_for_odin_initialised(timeout)
|
|
130
132
|
frames_dropped, frames_dropped_details = self.nodes.check_frames_dropped()
|
|
131
133
|
frames_timed_out, frames_timed_out_details = self.nodes.check_frames_timed_out()
|
|
132
134
|
|
|
@@ -139,22 +141,29 @@ class EigerOdin(Device):
|
|
|
139
141
|
|
|
140
142
|
return is_initialised and not frames_dropped and not frames_timed_out
|
|
141
143
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
144
|
+
def wait_for_odin_initialised(self, timeout) -> tuple[bool, str]:
|
|
145
|
+
errors = self.nodes.wait_for_no_errors(timeout)
|
|
146
|
+
await_true = partial(await_value, expected_value=True, timeout=timeout)
|
|
147
|
+
errors[
|
|
148
|
+
await_value(
|
|
149
|
+
self.fan.consumers_connected, expected_value=True, timeout=timeout
|
|
150
|
+
)
|
|
151
|
+
] = "EigerFan is not connected"
|
|
152
|
+
errors[await_true(self.fan.on)] = "EigerFan is not initialised"
|
|
153
|
+
errors[await_true(self.meta.initialised)] = "MetaListener is not initialised"
|
|
154
|
+
errors[self.nodes.get_init_state(timeout)] = (
|
|
155
|
+
"One or more filewriters is not initialised"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
error_strings = []
|
|
159
|
+
|
|
160
|
+
for error_status, string in errors.items():
|
|
161
|
+
try:
|
|
162
|
+
error_status.wait(timeout=timeout)
|
|
163
|
+
except Exception:
|
|
164
|
+
error_strings.append(string)
|
|
165
|
+
|
|
166
|
+
return not error_strings, "\n".join(error_strings)
|
|
158
167
|
|
|
159
168
|
def stop(self) -> StatusBase:
|
|
160
169
|
"""Stop odin manually"""
|
dodal/devices/focusing_mirror.py
CHANGED
|
@@ -45,7 +45,7 @@ class MirrorVoltageDemand(str, Enum):
|
|
|
45
45
|
SLEW = "SLEW"
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
class
|
|
48
|
+
class SingleMirrorVoltage(Device):
|
|
49
49
|
"""Abstract the bimorph mirror voltage PVs into a single device that can be set asynchronously and returns when
|
|
50
50
|
the demanded voltage setpoint is accepted, without blocking the caller as this process can take significant time.
|
|
51
51
|
"""
|
|
@@ -105,22 +105,28 @@ class MirrorVoltageDevice(Device):
|
|
|
105
105
|
await set_status
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
class
|
|
108
|
+
class MirrorVoltages(StandardReadable):
|
|
109
109
|
def __init__(
|
|
110
110
|
self, name: str, prefix: str, *args, daq_configuration_path: str, **kwargs
|
|
111
111
|
):
|
|
112
112
|
self.voltage_lookup_table_path = (
|
|
113
113
|
daq_configuration_path + "/json/mirrorFocus.json"
|
|
114
114
|
)
|
|
115
|
+
|
|
115
116
|
with self.add_children_as_readables():
|
|
116
|
-
self.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
for i in range(14, 22)
|
|
120
|
-
}
|
|
121
|
-
)
|
|
117
|
+
self.horizontal_voltages = self._channels_in_range(prefix, 0, 14)
|
|
118
|
+
self.vertical_voltages = self._channels_in_range(prefix, 14, 22)
|
|
119
|
+
|
|
122
120
|
super().__init__(*args, name=name, **kwargs)
|
|
123
121
|
|
|
122
|
+
def _channels_in_range(self, prefix, start_idx, end_idx):
|
|
123
|
+
return DeviceVector(
|
|
124
|
+
{
|
|
125
|
+
i - start_idx: SingleMirrorVoltage(prefix=f"{prefix}BM:V{i}")
|
|
126
|
+
for i in range(start_idx, end_idx)
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
124
130
|
|
|
125
131
|
class FocusingMirror(StandardReadable):
|
|
126
132
|
"""Focusing Mirror"""
|