dls-dodal 1.29.4__py3-none-any.whl → 1.31.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 (103) hide show
  1. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/METADATA +29 -44
  2. dls_dodal-1.31.0.dist-info/RECORD +134 -0
  3. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/WHEEL +1 -1
  4. dls_dodal-1.31.0.dist-info/entry_points.txt +3 -0
  5. dodal/__init__.py +1 -4
  6. dodal/_version.py +2 -2
  7. dodal/beamline_specific_utils/i03.py +1 -4
  8. dodal/beamlines/__init__.py +7 -1
  9. dodal/beamlines/i03.py +34 -29
  10. dodal/beamlines/i04.py +39 -16
  11. dodal/beamlines/i13_1.py +66 -0
  12. dodal/beamlines/i22.py +22 -22
  13. dodal/beamlines/i24.py +1 -1
  14. dodal/beamlines/p38.py +21 -21
  15. dodal/beamlines/p45.py +18 -16
  16. dodal/beamlines/p99.py +61 -0
  17. dodal/beamlines/training_rig.py +64 -0
  18. dodal/cli.py +6 -3
  19. dodal/common/beamlines/beamline_parameters.py +7 -6
  20. dodal/common/beamlines/beamline_utils.py +15 -14
  21. dodal/common/maths.py +1 -3
  22. dodal/common/types.py +6 -5
  23. dodal/common/udc_directory_provider.py +39 -21
  24. dodal/common/visit.py +60 -62
  25. dodal/devices/CTAB.py +22 -17
  26. dodal/devices/aperture.py +1 -1
  27. dodal/devices/aperturescatterguard.py +139 -209
  28. dodal/devices/areadetector/adaravis.py +8 -6
  29. dodal/devices/areadetector/adsim.py +2 -3
  30. dodal/devices/areadetector/adutils.py +20 -12
  31. dodal/devices/areadetector/plugins/MJPG.py +2 -1
  32. dodal/devices/backlight.py +12 -1
  33. dodal/devices/cryostream.py +19 -7
  34. dodal/devices/dcm.py +1 -1
  35. dodal/devices/detector/__init__.py +13 -2
  36. dodal/devices/detector/det_dim_constants.py +2 -2
  37. dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
  38. dodal/devices/detector/detector.py +33 -32
  39. dodal/devices/detector/detector_motion.py +38 -31
  40. dodal/devices/eiger.py +11 -15
  41. dodal/devices/eiger_odin.py +9 -10
  42. dodal/devices/fast_grid_scan.py +18 -27
  43. dodal/devices/fluorescence_detector_motion.py +13 -4
  44. dodal/devices/focusing_mirror.py +6 -6
  45. dodal/devices/hutch_shutter.py +4 -4
  46. dodal/devices/i22/dcm.py +5 -4
  47. dodal/devices/i22/fswitch.py +10 -6
  48. dodal/devices/i22/nxsas.py +55 -43
  49. dodal/devices/i24/aperture.py +1 -1
  50. dodal/devices/i24/beamstop.py +1 -1
  51. dodal/devices/i24/dcm.py +1 -1
  52. dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +1 -1
  53. dodal/devices/i24/pmac.py +67 -12
  54. dodal/devices/ipin.py +7 -4
  55. dodal/devices/linkam3.py +12 -6
  56. dodal/devices/logging_ophyd_device.py +1 -1
  57. dodal/devices/motors.py +32 -6
  58. dodal/devices/oav/grid_overlay.py +1 -0
  59. dodal/devices/oav/microns_for_zoom_levels.json +1 -1
  60. dodal/devices/oav/oav_detector.py +2 -1
  61. dodal/devices/oav/oav_parameters.py +18 -10
  62. dodal/devices/oav/oav_to_redis_forwarder.py +129 -0
  63. dodal/devices/oav/pin_image_recognition/__init__.py +6 -6
  64. dodal/devices/oav/pin_image_recognition/utils.py +5 -6
  65. dodal/devices/oav/utils.py +2 -2
  66. dodal/devices/p99/__init__.py +0 -0
  67. dodal/devices/p99/sample_stage.py +43 -0
  68. dodal/devices/robot.py +31 -20
  69. dodal/devices/scatterguard.py +1 -1
  70. dodal/devices/scintillator.py +8 -5
  71. dodal/devices/slits.py +1 -1
  72. dodal/devices/smargon.py +4 -4
  73. dodal/devices/status.py +2 -31
  74. dodal/devices/tetramm.py +23 -19
  75. dodal/devices/thawer.py +5 -3
  76. dodal/devices/training_rig/__init__.py +0 -0
  77. dodal/devices/training_rig/sample_stage.py +10 -0
  78. dodal/devices/turbo_slit.py +1 -1
  79. dodal/devices/undulator.py +1 -1
  80. dodal/devices/undulator_dcm.py +6 -8
  81. dodal/devices/util/adjuster_plans.py +3 -3
  82. dodal/devices/util/epics_util.py +5 -7
  83. dodal/devices/util/lookup_tables.py +2 -3
  84. dodal/devices/util/save_panda.py +87 -0
  85. dodal/devices/util/test_utils.py +17 -0
  86. dodal/devices/webcam.py +3 -3
  87. dodal/devices/xbpm_feedback.py +1 -25
  88. dodal/devices/xspress3/xspress3.py +1 -1
  89. dodal/devices/zebra.py +15 -10
  90. dodal/devices/zebra_controlled_shutter.py +26 -11
  91. dodal/devices/zocalo/zocalo_interaction.py +10 -2
  92. dodal/devices/zocalo/zocalo_results.py +36 -19
  93. dodal/log.py +46 -15
  94. dodal/plans/check_topup.py +65 -10
  95. dodal/plans/data_session_metadata.py +8 -9
  96. dodal/plans/motor_util_plans.py +117 -0
  97. dodal/utils.py +65 -22
  98. dls_dodal-1.29.4.dist-info/RECORD +0 -125
  99. dls_dodal-1.29.4.dist-info/entry_points.txt +0 -2
  100. dodal/devices/beamstop.py +0 -8
  101. dodal/devices/qbpm1.py +0 -8
  102. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/LICENSE +0 -0
  103. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,39 @@
1
+ import asyncio
2
+ from collections.abc import Awaitable, Iterable
1
3
  from dataclasses import dataclass, fields
2
- from typing import Dict
4
+ from typing import TypeVar
3
5
 
4
6
  from bluesky.protocols import Reading
5
7
  from event_model.documents.event_descriptor import DataKey
6
- from ophyd_async.core import DirectoryProvider, merge_gathered_dicts
7
- from ophyd_async.epics.areadetector import AravisDetector, PilatusDetector
8
- from ophyd_async.epics.areadetector.aravis import AravisController
8
+ from ophyd_async.core import PathProvider
9
+ from ophyd_async.epics.adaravis import AravisController, AravisDetector
10
+ from ophyd_async.epics.adpilatus import PilatusDetector
9
11
 
10
12
  ValueAndUnits = tuple[float, str]
13
+ T = TypeVar("T")
14
+
15
+
16
+ # TODO: Remove this file as part of github.com/DiamondLightSource/dodal/issues/595
17
+ # Until which, temporarily duplicated non-public method from ophyd_async
18
+ async def _merge_gathered_dicts(
19
+ coros: Iterable[Awaitable[dict[str, T]]],
20
+ ) -> dict[str, T]:
21
+ """Merge dictionaries produced by a sequence of coroutines.
22
+
23
+ Can be used for merging ``read()`` or ``describe``. For instance::
24
+
25
+ combined_read = await merge_gathered_dicts(s.read() for s in signals)
26
+ """
27
+ ret: dict[str, T] = {}
28
+ for result in await asyncio.gather(*coros):
29
+ ret.update(result)
30
+ return ret
11
31
 
12
32
 
13
33
  @dataclass
14
34
  class MetadataHolder:
15
35
  # TODO: just in case this is useful more widely...
16
- async def describe(self, parent_name: str) -> Dict[str, DataKey]:
36
+ async def describe(self, parent_name: str) -> dict[str, DataKey]:
17
37
  def datakey(value) -> DataKey:
18
38
  if isinstance(value, tuple):
19
39
  return {"units": value[1], **datakey(value[0])}
@@ -40,8 +60,8 @@ class MetadataHolder:
40
60
  if getattr(self, field.name, None) is not None
41
61
  }
42
62
 
43
- async def read(self, parent_name: str) -> Dict[str, Reading]:
44
- def reading(value):
63
+ async def read(self, parent_name: str) -> dict[str, Reading]:
64
+ def reading(value) -> Reading:
45
65
  if isinstance(value, tuple):
46
66
  return reading(value[0])
47
67
  return {"timestamp": -1, "value": value}
@@ -81,7 +101,7 @@ class NXSasPilatus(PilatusDetector):
81
101
  def __init__(
82
102
  self,
83
103
  prefix: str,
84
- directory_provider: DirectoryProvider,
104
+ path_provider: PathProvider,
85
105
  drv_suffix: str,
86
106
  hdf_suffix: str,
87
107
  metadata_holder: NXSasMetadataHolder,
@@ -94,32 +114,28 @@ class NXSasPilatus(PilatusDetector):
94
114
  Writes hdf5 files."""
95
115
  super().__init__(
96
116
  prefix,
97
- directory_provider,
117
+ path_provider,
98
118
  drv_suffix=drv_suffix,
99
119
  hdf_suffix=hdf_suffix,
100
120
  name=name,
101
121
  )
102
122
  self._metadata_holder = metadata_holder
103
123
 
104
- async def read_configuration(self) -> Dict[str, Reading]:
105
- return await merge_gathered_dicts(
106
- (
107
- r
108
- for r in (
109
- super().read_configuration(),
110
- self._metadata_holder.read(self.name),
111
- )
124
+ async def read_configuration(self) -> dict[str, Reading]:
125
+ return await _merge_gathered_dicts(
126
+ r
127
+ for r in (
128
+ super().read_configuration(),
129
+ self._metadata_holder.read(self.name),
112
130
  )
113
131
  )
114
132
 
115
- async def describe_configuration(self) -> Dict[str, DataKey]:
116
- return await merge_gathered_dicts(
117
- (
118
- r
119
- for r in (
120
- super().describe_configuration(),
121
- self._metadata_holder.describe(self.name),
122
- )
133
+ async def describe_configuration(self) -> dict[str, DataKey]:
134
+ return await _merge_gathered_dicts(
135
+ r
136
+ for r in (
137
+ super().describe_configuration(),
138
+ self._metadata_holder.describe(self.name),
123
139
  )
124
140
  )
125
141
 
@@ -128,7 +144,7 @@ class NXSasOAV(AravisDetector):
128
144
  def __init__(
129
145
  self,
130
146
  prefix: str,
131
- directory_provider: DirectoryProvider,
147
+ path_provider: PathProvider,
132
148
  drv_suffix: str,
133
149
  hdf_suffix: str,
134
150
  metadata_holder: NXSasMetadataHolder,
@@ -142,7 +158,7 @@ class NXSasOAV(AravisDetector):
142
158
  Writes hdf5 files."""
143
159
  super().__init__(
144
160
  prefix,
145
- directory_provider,
161
+ path_provider,
146
162
  drv_suffix=drv_suffix,
147
163
  hdf_suffix=hdf_suffix,
148
164
  name=name,
@@ -150,24 +166,20 @@ class NXSasOAV(AravisDetector):
150
166
  )
151
167
  self._metadata_holder = metadata_holder
152
168
 
153
- async def read_configuration(self) -> Dict[str, Reading]:
154
- return await merge_gathered_dicts(
155
- (
156
- r
157
- for r in (
158
- super().read_configuration(),
159
- self._metadata_holder.read(self.name),
160
- )
169
+ async def read_configuration(self) -> dict[str, Reading]:
170
+ return await _merge_gathered_dicts(
171
+ r
172
+ for r in (
173
+ super().read_configuration(),
174
+ self._metadata_holder.read(self.name),
161
175
  )
162
176
  )
163
177
 
164
- async def describe_configuration(self) -> Dict[str, DataKey]:
165
- return await merge_gathered_dicts(
166
- (
167
- r
168
- for r in (
169
- super().describe_configuration(),
170
- self._metadata_holder.describe(self.name),
171
- )
178
+ async def describe_configuration(self) -> dict[str, DataKey]:
179
+ return await _merge_gathered_dicts(
180
+ r
181
+ for r in (
182
+ super().describe_configuration(),
183
+ self._metadata_holder.describe(self.name),
172
184
  )
173
185
  )
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
3
  from ophyd_async.core import StandardReadable
4
- from ophyd_async.epics.motion import Motor
4
+ from ophyd_async.epics.motor import Motor
5
5
  from ophyd_async.epics.signal import epics_signal_rw
6
6
 
7
7
 
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
3
  from ophyd_async.core import StandardReadable
4
- from ophyd_async.epics.motion import Motor
4
+ from ophyd_async.epics.motor import Motor
5
5
  from ophyd_async.epics.signal import epics_signal_rw
6
6
 
7
7
 
dodal/devices/i24/dcm.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from ophyd_async.core import StandardReadable
2
- from ophyd_async.epics.motion import Motor
2
+ from ophyd_async.epics.motor import Motor
3
3
  from ophyd_async.epics.signal import epics_signal_r
4
4
 
5
5
 
@@ -1,5 +1,5 @@
1
1
  from ophyd_async.core import StandardReadable
2
- from ophyd_async.epics.motion import Motor
2
+ from ophyd_async.epics.motor import Motor
3
3
 
4
4
 
5
5
  class DetectorMotion(StandardReadable):
dodal/devices/i24/pmac.py CHANGED
@@ -1,18 +1,30 @@
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
6
- from ophyd_async.core.signal_backend import SignalBackend
7
- from ophyd_async.core.soft_signal_backend import SoftSignalBackend
8
- from ophyd_async.core.utils import DEFAULT_TIMEOUT
9
- from ophyd_async.epics.motion import Motor
5
+ from ophyd_async.core import (
6
+ DEFAULT_TIMEOUT,
7
+ AsyncStatus,
8
+ CalculateTimeout,
9
+ SignalBackend,
10
+ SignalR,
11
+ SignalRW,
12
+ SoftSignalBackend,
13
+ StandardReadable,
14
+ wait_for_value,
15
+ )
16
+ from ophyd_async.epics.motor import Motor
10
17
  from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
11
18
 
12
19
  HOME_STR = r"\#1hmz\#2hmz\#3hmz" # Command to home the PMAC motors
13
20
  ZERO_STR = "!x0y0z0" # Command to blend any ongoing move into new position
14
21
 
15
22
 
23
+ class ScanState(IntEnum):
24
+ RUNNING = 1
25
+ DONE = 0
26
+
27
+
16
28
  class LaserSettings(str, Enum):
17
29
  """PMAC strings to switch laser on and off.
18
30
  Note. On the PMAC, M-variables usually have to do with position compare
@@ -73,26 +85,65 @@ class PMACStringLaser(SignalRW):
73
85
  super().__init__(backend, timeout, name)
74
86
 
75
87
  @AsyncStatus.wrap
76
- async def set(self, laser_setting: LaserSettings):
77
- await self.signal.set(laser_setting.value, wait=True)
88
+ async def set(
89
+ self,
90
+ value: LaserSettings,
91
+ wait=True,
92
+ timeout=CalculateTimeout,
93
+ ):
94
+ await self.signal.set(value.value, wait, timeout)
78
95
 
79
96
 
80
97
  class PMACStringEncReset(SignalRW):
81
- """"""
98
+ """Set a pmac_string to control the encoder channels in the controller."""
99
+
100
+ def __init__(
101
+ self,
102
+ pmac_str_sig: SignalRW,
103
+ backend: SignalBackend,
104
+ timeout: float | None = DEFAULT_TIMEOUT,
105
+ name: str = "",
106
+ ) -> None:
107
+ self.signal = pmac_str_sig
108
+ super().__init__(backend, timeout, name)
109
+
110
+ @AsyncStatus.wrap
111
+ async def set(
112
+ self,
113
+ value: EncReset,
114
+ wait=True,
115
+ timeout=CalculateTimeout,
116
+ ):
117
+ await self.signal.set(value.value, wait, timeout)
118
+
119
+
120
+ class ProgramRunner(SignalRW):
121
+ """Trigger the collection by setting the program number on the PMAC string.
122
+
123
+ Once the program number has been set, wait for the collection to be complete.
124
+ This will only be true when the status becomes 0.
125
+ """
82
126
 
83
127
  def __init__(
84
128
  self,
85
129
  pmac_str_sig: SignalRW,
130
+ status_sig: SignalR,
86
131
  backend: SignalBackend,
87
132
  timeout: float | None = DEFAULT_TIMEOUT,
88
133
  name: str = "",
89
134
  ) -> None:
90
135
  self.signal = pmac_str_sig
136
+ self.status = status_sig
91
137
  super().__init__(backend, timeout, name)
92
138
 
93
139
  @AsyncStatus.wrap
94
- async def set(self, enc_string: EncReset):
95
- await self.signal.set(enc_string.value, wait=True)
140
+ async def set(self, value: int, wait=True, timeout=None):
141
+ prog_str = f"&2b{value}r"
142
+ assert isinstance(timeout, SupportsFloat) or (
143
+ timeout is None
144
+ ), f"ProgramRunner does not support calculating timeout itself, {timeout=}"
145
+ await self.signal.set(prog_str, wait=wait)
146
+ await wait_for_value(self.status, ScanState.DONE, timeout)
96
147
 
97
148
 
98
149
  class PMAC(StandardReadable):
@@ -121,4 +172,8 @@ class PMAC(StandardReadable):
121
172
  self.scanstatus = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2401")
122
173
  self.counter = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2402")
123
174
 
175
+ self.run_program = ProgramRunner(
176
+ self.pmac_string, self.scanstatus, backend=SoftSignalBackend(str)
177
+ )
178
+
124
179
  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,11 +1,16 @@
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
8
- from ophyd_async.core.utils import WatcherUpdate
6
+ from ophyd_async.core import (
7
+ ConfigSignal,
8
+ HintedSignal,
9
+ StandardReadable,
10
+ WatchableAsyncStatus,
11
+ WatcherUpdate,
12
+ observe_value,
13
+ )
9
14
  from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
10
15
 
11
16
 
@@ -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
@@ -1,10 +1,36 @@
1
1
  from ophyd_async.core import Device
2
- from ophyd_async.epics.motion import Motor
2
+ from ophyd_async.epics.motor 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,7 +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):
49
- self.parent: "OAV"
50
+ self.parent: OAV
50
51
  flat_applied = self.parent.proc.port_name.get()
51
52
  no_flat_applied = self.parent.cam.port_name.get()
52
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,129 @@
1
+ import asyncio
2
+ import io
3
+ import pickle
4
+ import uuid
5
+ from collections.abc import Awaitable, Callable
6
+ from datetime import timedelta
7
+
8
+ import numpy as np
9
+ from aiohttp import ClientResponse, ClientSession
10
+ from bluesky.protocols import Flyable, Stoppable
11
+ from ophyd_async.core import (
12
+ AsyncStatus,
13
+ StandardReadable,
14
+ soft_signal_r_and_setter,
15
+ soft_signal_rw,
16
+ )
17
+ from ophyd_async.epics.signal import epics_signal_r
18
+ from PIL import Image
19
+ from redis.asyncio import StrictRedis
20
+
21
+ from dodal.log import LOGGER
22
+
23
+
24
+ async def get_next_jpeg(response: ClientResponse) -> bytes:
25
+ JPEG_START_BYTE = b"\xff\xd8"
26
+ JPEG_STOP_BYTE = b"\xff\xd9"
27
+ while True:
28
+ line = await response.content.readline()
29
+ if line.startswith(JPEG_START_BYTE):
30
+ return line + await response.content.readuntil(JPEG_STOP_BYTE)
31
+
32
+
33
+ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
34
+ """Forwards OAV image data to redis. To use call:
35
+
36
+ > bps.kickoff(oav_forwarder)
37
+ > bps.monitor(oav_forwarder.uuid)
38
+ > bps.complete(oav_forwarder)
39
+
40
+ """
41
+
42
+ DATA_EXPIRY_DAYS = 7
43
+
44
+ def __init__(
45
+ self,
46
+ prefix: str,
47
+ redis_host: str,
48
+ redis_password: str,
49
+ redis_db: int = 0,
50
+ name: str = "",
51
+ ) -> None:
52
+ """Reads image data from the MJPEG stream on an OAV and forwards it into a
53
+ redis database. This is currently only used for murko integration.
54
+
55
+ Arguments:
56
+ prefix: str the PV prefix of the OAV
57
+ redis_host: str the host where the redis database is running
58
+ redis_password: str the password for the redis database
59
+ redis_db: int which redis database to connect to, defaults to 0
60
+ name: str the name of this device
61
+ """
62
+ self.stream_url = epics_signal_r(str, f"{prefix}MJPG:MJPG_URL_RBV")
63
+
64
+ with self.add_children_as_readables():
65
+ self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
66
+
67
+ self.forwarding_task = None
68
+ self.redis_client = StrictRedis(
69
+ host=redis_host, password=redis_password, db=redis_db
70
+ )
71
+
72
+ self._stop_flag = False
73
+
74
+ self.sample_id = soft_signal_rw(int, initial_value=0)
75
+
76
+ # The uuid that images are being saved under, this should be monitored for
77
+ # callbacks to correlate the data
78
+ self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
79
+
80
+ super().__init__(name=name)
81
+
82
+ async def _get_frame_and_put_to_redis(self, response: ClientResponse):
83
+ """Converts the data that comes in as a jpeg byte stream into a numpy array of
84
+ RGB values, pickles this array then writes it to redis.
85
+ """
86
+ jpeg_bytes = await get_next_jpeg(response)
87
+ self.uuid_setter(image_uuid := str(uuid.uuid4()))
88
+ img = Image.open(io.BytesIO(jpeg_bytes))
89
+ image_data = pickle.dumps(np.asarray(img))
90
+ sample_id = str(await self.sample_id.get_value())
91
+ await self.redis_client.hset(sample_id, image_uuid, image_data) # type: ignore
92
+ await self.redis_client.expire(sample_id, timedelta(days=self.DATA_EXPIRY_DAYS))
93
+ LOGGER.debug(f"Sent frame to redis key {sample_id} with uuid {image_uuid}")
94
+
95
+ async def _open_connection_and_do_function(
96
+ self, function_to_do: Callable[[ClientResponse, str | None], Awaitable]
97
+ ):
98
+ stream_url = await self.stream_url.get_value()
99
+ async with ClientSession() as session:
100
+ async with session.get(stream_url) as response:
101
+ await function_to_do(response, stream_url)
102
+
103
+ async def _stream_to_redis(self, response, _):
104
+ while not self._stop_flag:
105
+ await self._get_frame_and_put_to_redis(response)
106
+ await asyncio.sleep(0.01)
107
+
108
+ async def _confirm_mjpg_stream(self, response, stream_url):
109
+ if response.content_type != "multipart/x-mixed-replace":
110
+ raise ValueError(f"{stream_url} is not an MJPG stream")
111
+
112
+ @AsyncStatus.wrap
113
+ async def kickoff(self):
114
+ self._stop_flag = False
115
+ await self._open_connection_and_do_function(self._confirm_mjpg_stream)
116
+ self.forwarding_task = asyncio.create_task(
117
+ self._open_connection_and_do_function(self._stream_to_redis)
118
+ )
119
+
120
+ @AsyncStatus.wrap
121
+ async def complete(self):
122
+ assert self.forwarding_task, "Device not kicked off"
123
+ await self.stop()
124
+
125
+ @AsyncStatus.wrap
126
+ async def stop(self, success=True):
127
+ if self.forwarding_task:
128
+ self._stop_flag = True
129
+ await self.forwarding_task