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.
Files changed (88) hide show
  1. {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/METADATA +28 -43
  2. dls_dodal-1.30.0.dist-info/RECORD +132 -0
  3. {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/WHEEL +1 -1
  4. dls_dodal-1.30.0.dist-info/entry_points.txt +3 -0
  5. dodal/__init__.py +1 -4
  6. dodal/_version.py +2 -2
  7. dodal/beamlines/__init__.py +3 -1
  8. dodal/beamlines/i03.py +28 -23
  9. dodal/beamlines/i04.py +34 -12
  10. dodal/beamlines/i13_1.py +66 -0
  11. dodal/beamlines/i22.py +5 -5
  12. dodal/beamlines/i24.py +15 -1
  13. dodal/beamlines/p38.py +7 -7
  14. dodal/beamlines/p45.py +7 -5
  15. dodal/beamlines/p99.py +61 -0
  16. dodal/cli.py +6 -3
  17. dodal/common/beamlines/beamline_parameters.py +2 -2
  18. dodal/common/beamlines/beamline_utils.py +6 -5
  19. dodal/common/maths.py +1 -3
  20. dodal/common/types.py +2 -3
  21. dodal/common/udc_directory_provider.py +14 -3
  22. dodal/common/visit.py +2 -3
  23. dodal/devices/CTAB.py +22 -17
  24. dodal/devices/aperturescatterguard.py +114 -136
  25. dodal/devices/areadetector/adaravis.py +8 -6
  26. dodal/devices/areadetector/adsim.py +2 -3
  27. dodal/devices/areadetector/adutils.py +20 -12
  28. dodal/devices/areadetector/plugins/MJPG.py +0 -4
  29. dodal/devices/attenuator.py +4 -4
  30. dodal/devices/cryostream.py +19 -7
  31. dodal/devices/detector/__init__.py +13 -2
  32. dodal/devices/detector/det_dim_constants.py +2 -2
  33. dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
  34. dodal/devices/detector/detector.py +8 -7
  35. dodal/devices/detector/detector_motion.py +38 -31
  36. dodal/devices/eiger.py +23 -23
  37. dodal/devices/eiger_odin.py +12 -13
  38. dodal/devices/fast_grid_scan.py +4 -3
  39. dodal/devices/fluorescence_detector_motion.py +13 -4
  40. dodal/devices/focusing_mirror.py +66 -66
  41. dodal/devices/hutch_shutter.py +4 -4
  42. dodal/devices/i22/dcm.py +4 -3
  43. dodal/devices/i22/fswitch.py +4 -4
  44. dodal/devices/i22/nxsas.py +23 -32
  45. dodal/devices/i24/dcm.py +42 -0
  46. dodal/devices/i24/pmac.py +47 -8
  47. dodal/devices/ipin.py +7 -4
  48. dodal/devices/linkam3.py +11 -5
  49. dodal/devices/logging_ophyd_device.py +1 -1
  50. dodal/devices/motors.py +31 -5
  51. dodal/devices/oav/grid_overlay.py +1 -0
  52. dodal/devices/oav/microns_for_zoom_levels.json +1 -1
  53. dodal/devices/oav/oav_detector.py +2 -0
  54. dodal/devices/oav/oav_parameters.py +18 -10
  55. dodal/devices/oav/oav_to_redis_forwarder.py +100 -0
  56. dodal/devices/oav/pin_image_recognition/__init__.py +19 -17
  57. dodal/devices/oav/pin_image_recognition/utils.py +5 -6
  58. dodal/devices/oav/utils.py +3 -17
  59. dodal/devices/p99/__init__.py +0 -0
  60. dodal/devices/p99/sample_stage.py +43 -0
  61. dodal/devices/robot.py +30 -18
  62. dodal/devices/scintillator.py +8 -5
  63. dodal/devices/smargon.py +3 -3
  64. dodal/devices/status.py +2 -31
  65. dodal/devices/tetramm.py +4 -4
  66. dodal/devices/thawer.py +5 -3
  67. dodal/devices/undulator_dcm.py +6 -8
  68. dodal/devices/util/adjuster_plans.py +2 -2
  69. dodal/devices/util/epics_util.py +6 -8
  70. dodal/devices/util/lookup_tables.py +2 -3
  71. dodal/devices/util/save_panda.py +87 -0
  72. dodal/devices/util/test_utils.py +17 -0
  73. dodal/devices/webcam.py +3 -8
  74. dodal/devices/xbpm_feedback.py +0 -23
  75. dodal/devices/zebra.py +10 -10
  76. dodal/devices/zebra_controlled_shutter.py +3 -3
  77. dodal/devices/zocalo/zocalo_interaction.py +10 -2
  78. dodal/devices/zocalo/zocalo_results.py +31 -18
  79. dodal/log.py +14 -5
  80. dodal/plans/data_session_metadata.py +1 -0
  81. dodal/plans/motor_util_plans.py +117 -0
  82. dodal/utils.py +74 -26
  83. dls_dodal-1.29.3.dist-info/RECORD +0 -124
  84. dls_dodal-1.29.3.dist-info/entry_points.txt +0 -2
  85. dodal/devices/qbpm1.py +0 -8
  86. {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/LICENSE +0 -0
  87. {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/top_level.txt +0 -0
  88. /dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +0 -0
@@ -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) -> Dict[str, DataKey]:
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) -> Dict[str, Reading]:
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) -> Dict[str, Reading]:
103
+ async def read_configuration(self) -> dict[str, Reading]:
105
104
  return await merge_gathered_dicts(
106
- (
107
- r
108
- for r in (
109
- super().read_configuration(),
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) -> Dict[str, DataKey]:
112
+ async def describe_configuration(self) -> dict[str, DataKey]:
116
113
  return await merge_gathered_dicts(
117
- (
118
- r
119
- for r in (
120
- super().describe_configuration(),
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) -> Dict[str, Reading]:
148
+ async def read_configuration(self) -> dict[str, Reading]:
154
149
  return await merge_gathered_dicts(
155
- (
156
- r
157
- for r in (
158
- super().read_configuration(),
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) -> Dict[str, DataKey]:
157
+ async def describe_configuration(self) -> dict[str, DataKey]:
165
158
  return await merge_gathered_dicts(
166
- (
167
- r
168
- for r in (
169
- super().describe_configuration(),
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
  )
@@ -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, laser_setting: LaserSettings):
77
- await self.signal.set(laser_setting.value, wait=True)
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, enc_string: EncReset):
95
- await self.signal.set(enc_string.value, wait=True)
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 ophyd import Component as Cpt
2
- from ophyd import Device, EpicsSignalRO, Kind
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(Device):
5
+ class IPin(StandardReadable):
6
6
  """Simple device to get the ipin reading"""
7
7
 
8
- reading = Cpt(EpicsSignalRO, "I", kind=Kind.hinted)
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 StandardReadable, WatchableAsyncStatus, observe_value
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.set_readable_signals(
61
- read=(self.temp,), config=(self.ramp_rate, self.speed, self.set_point)
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: Optional[float] = None):
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
- def __init__(self, prefix: str, name: str):
7
- self.x = Motor(prefix + "X")
8
- self.y = Motor(prefix + "Y")
9
- self.z = Motor(prefix + "Z")
10
- super().__init__(name)
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 enum import Enum
2
3
  from functools import partial
3
4
  from os.path import join as path_join
@@ -52,4 +52,4 @@
52
52
  "micronsPerXPixel": 0.227,
53
53
  "micronsPerYPixel": 0.314
54
54
  }
55
- }
55
+ }
@@ -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.cElementTree as et
2
+ import xml.etree.ElementTree as et
3
3
  from collections import ChainMap
4
- from typing import Any, Tuple
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 float(node.find("level").text) == zoom:
145
+ if _get_element_as_float(node, "level") == zoom:
138
146
  self.micronsPerXPixel = (
139
- float(node.find("micronsPerXPixel").text)
147
+ _get_element_as_float(node, "micronsPerXPixel")
140
148
  * DEFAULT_OAV_WINDOW[0]
141
149
  / xsize
142
150
  )
143
151
  self.micronsPerYPixel = (
144
- float(node.find("micronsPerYPixel").text)
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
- ) -> Tuple[int, int]:
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, "r") as f:
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
- ) -> Tuple[int, int]:
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, _ = soft_signal_r_and_setter(Tip, name="triggered_tip")
54
- self.triggered_top_edge, _ = soft_signal_r_and_setter(
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, _ = soft_signal_r_and_setter(
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.set_readable_signals(
79
- read=[
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
- async def _set_triggered_values(self, results: SampleLocation):
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
- await self.triggered_tip._backend.put(tip)
94
- await self.triggered_top_edge._backend.put(results.edge_top)
95
- await self.triggered_bottom_edge._backend.put(results.edge_bottom)
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".format(
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
- await self._set_triggered_values(location)
155
+ self._set_triggered_values(location)
154
156
  except Exception as e:
155
- LOGGER.warn(
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
- await self.triggered_tip._backend.put(self.INVALID_POSITION)
170
- await self.triggered_bottom_edge._backend.put(np.array([]))
171
- await self.triggered_top_edge._backend.put(np.array([]))
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 Callable, Final, Tuple
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(object):
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
- ) -> Tuple[np.ndarray, np.ndarray]:
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={})".format(
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