dls-dodal 1.33.0__py3-none-any.whl → 1.35.0__py3-none-any.whl

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