dls-dodal 1.68.0__py3-none-any.whl → 1.69.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 (57) hide show
  1. {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/METADATA +1 -31
  2. {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/RECORD +57 -49
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/adsim.py +30 -23
  5. dodal/beamlines/i02_1.py +14 -42
  6. dodal/beamlines/i02_2.py +5 -11
  7. dodal/beamlines/i03.py +4 -1
  8. dodal/beamlines/i03_supervisor.py +19 -0
  9. dodal/beamlines/i04.py +74 -179
  10. dodal/beamlines/i05.py +8 -0
  11. dodal/beamlines/i06_1.py +24 -0
  12. dodal/beamlines/i09.py +53 -9
  13. dodal/beamlines/i09_1.py +8 -0
  14. dodal/beamlines/i09_2.py +4 -4
  15. dodal/beamlines/i16.py +11 -0
  16. dodal/beamlines/i20_1.py +14 -0
  17. dodal/beamlines/i21.py +12 -4
  18. dodal/beamlines/i23.py +19 -25
  19. dodal/beamlines/i24.py +55 -105
  20. dodal/beamlines/p60.py +11 -1
  21. dodal/common/__init__.py +2 -1
  22. dodal/common/maths.py +80 -0
  23. dodal/devices/eiger.py +29 -14
  24. dodal/devices/electron_analyser/base/__init__.py +3 -3
  25. dodal/devices/electron_analyser/base/base_controller.py +19 -8
  26. dodal/devices/electron_analyser/base/base_enums.py +0 -5
  27. dodal/devices/electron_analyser/base/base_region.py +2 -1
  28. dodal/devices/electron_analyser/base/energy_sources.py +27 -26
  29. dodal/devices/electron_analyser/specs/specs_detector.py +7 -6
  30. dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +7 -6
  31. dodal/devices/fast_shutter.py +108 -25
  32. dodal/devices/i04/beam_centre.py +84 -0
  33. dodal/devices/i04/max_pixel.py +4 -17
  34. dodal/devices/i04/murko_results.py +18 -3
  35. dodal/devices/i10/i10_apple2.py +6 -6
  36. dodal/devices/i17/i17_apple2.py +6 -6
  37. dodal/devices/i24/commissioning_jungfrau.py +9 -10
  38. dodal/devices/insertion_device/__init__.py +12 -8
  39. dodal/devices/insertion_device/apple2_controller.py +380 -0
  40. dodal/devices/insertion_device/apple2_undulator.py +152 -531
  41. dodal/devices/insertion_device/energy.py +88 -0
  42. dodal/devices/insertion_device/energy_motor_lookup.py +1 -1
  43. dodal/devices/insertion_device/lookup_table_models.py +2 -2
  44. dodal/devices/insertion_device/polarisation.py +36 -0
  45. dodal/devices/oav/oav_detector.py +66 -1
  46. dodal/devices/oav/utils.py +17 -0
  47. dodal/devices/robot.py +35 -18
  48. dodal/devices/selectable_source.py +38 -0
  49. dodal/devices/zebra/zebra.py +15 -0
  50. dodal/devices/zebra/zebra_constants_mapping.py +1 -0
  51. dodal/plans/configure_arm_trigger_and_disarm_detector.py +0 -1
  52. dodal/testing/__init__.py +0 -0
  53. {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/WHEEL +0 -0
  54. {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/entry_points.txt +0 -0
  55. {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/licenses/LICENSE +0 -0
  56. {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/top_level.txt +0 -0
  57. /dodal/devices/insertion_device/{id_enum.py → enum.py} +0 -0
@@ -0,0 +1,88 @@
1
+ import abc
2
+ import asyncio
3
+
4
+ from bluesky.protocols import Movable
5
+ from ophyd_async.core import (
6
+ AsyncStatus,
7
+ Reference,
8
+ SignalRW,
9
+ StandardReadable,
10
+ StandardReadableFormat,
11
+ soft_signal_rw,
12
+ )
13
+ from ophyd_async.epics.motor import Motor
14
+
15
+ from dodal.devices.insertion_device import MAXIMUM_MOVE_TIME, Apple2Controller
16
+ from dodal.log import LOGGER
17
+
18
+
19
+ class InsertionDeviceEnergyBase(abc.ABC, StandardReadable, Movable):
20
+ """Base class for ID energy movable device."""
21
+
22
+ def __init__(self, name: str = "") -> None:
23
+ self.energy: Reference[SignalRW[float]]
24
+ super().__init__(name=name)
25
+
26
+ @abc.abstractmethod
27
+ @AsyncStatus.wrap
28
+ async def set(self, energy: float) -> None: ...
29
+
30
+
31
+ class BeamEnergy(StandardReadable, Movable[float]):
32
+ """
33
+ Compound device to set both ID and energy motor at the same time with an option to add an offset.
34
+ """
35
+
36
+ def __init__(
37
+ self, id_energy: InsertionDeviceEnergyBase, mono: Motor, name: str = ""
38
+ ) -> None:
39
+ """
40
+ Parameters
41
+ ----------
42
+
43
+ id_energy: InsertionDeviceEnergy
44
+ An InsertionDeviceEnergy device.
45
+ mono: Motor
46
+ A Motor(energy) device.
47
+ name:
48
+ New device name.
49
+ """
50
+ super().__init__(name=name)
51
+ self._id_energy = Reference(id_energy)
52
+ self._mono_energy = Reference(mono)
53
+
54
+ self.add_readables(
55
+ [
56
+ self._id_energy().energy(),
57
+ self._mono_energy().user_readback,
58
+ ],
59
+ StandardReadableFormat.HINTED_SIGNAL,
60
+ )
61
+
62
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
63
+ self.id_energy_offset = soft_signal_rw(float, initial_value=0)
64
+
65
+ @AsyncStatus.wrap
66
+ async def set(self, energy: float) -> None:
67
+ LOGGER.info(f"Moving f{self.name} energy to {energy}.")
68
+ await asyncio.gather(
69
+ self._id_energy().set(
70
+ energy=energy + await self.id_energy_offset.get_value()
71
+ ),
72
+ self._mono_energy().set(energy),
73
+ )
74
+
75
+
76
+ class InsertionDeviceEnergy(InsertionDeviceEnergyBase):
77
+ """Apple2 ID energy movable device."""
78
+
79
+ def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
80
+ self.energy = Reference(id_controller.energy)
81
+ super().__init__(name=name)
82
+
83
+ self.add_readables([self.energy()], StandardReadableFormat.HINTED_SIGNAL)
84
+
85
+ @AsyncStatus.wrap
86
+ async def set(self, energy: float) -> None:
87
+ LOGGER.info(f"Setting insertion device energy to {energy}.")
88
+ await self.energy().set(energy, timeout=MAXIMUM_MOVE_TIME)
@@ -2,7 +2,7 @@ from pathlib import Path
2
2
 
3
3
  from daq_config_server.client import ConfigServer
4
4
 
5
- from dodal.devices.insertion_device.id_enum import Pol
5
+ from dodal.devices.insertion_device.enum import Pol
6
6
  from dodal.devices.insertion_device.lookup_table_models import (
7
7
  LookupTable,
8
8
  LookupTableColumnConfig,
@@ -39,7 +39,7 @@ from pydantic import (
39
39
  field_validator,
40
40
  )
41
41
 
42
- from dodal.devices.insertion_device.id_enum import Pol
42
+ from dodal.devices.insertion_device.enum import Pol
43
43
 
44
44
  DEFAULT_POLY_DEG = [
45
45
  "7th-order",
@@ -57,7 +57,7 @@ DEFAULT_GAP_FILE = "IDEnergy2GapCalibrations.csv"
57
57
  DEFAULT_PHASE_FILE = "IDEnergy2PhaseCalibrations.csv"
58
58
 
59
59
  ROW_PHASE_MOTOR_TOLERANCE = 0.004
60
- ROW_PHASE_CIRCULAR = 15
60
+ ROW_PHASE_CIRCULAR = 15.0
61
61
  MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
62
62
  MAXIMUM_GAP_MOTOR_POSITION = 100
63
63
 
@@ -0,0 +1,36 @@
1
+ import asyncio
2
+
3
+ from bluesky.protocols import Locatable, Location
4
+ from ophyd_async.core import (
5
+ AsyncStatus,
6
+ Reference,
7
+ StandardReadable,
8
+ StandardReadableFormat,
9
+ )
10
+
11
+ from dodal.devices.insertion_device import MAXIMUM_MOVE_TIME, Apple2Controller
12
+ from dodal.devices.insertion_device.enum import Pol
13
+ from dodal.log import LOGGER
14
+
15
+
16
+ class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
17
+ """Apple2 ID polarisation movable device."""
18
+
19
+ def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
20
+ self.polarisation = Reference(id_controller.polarisation)
21
+ self.polarisation_setpoint = Reference(id_controller.polarisation_setpoint)
22
+ super().__init__(name=name)
23
+
24
+ self.add_readables([self.polarisation()], StandardReadableFormat.HINTED_SIGNAL)
25
+
26
+ @AsyncStatus.wrap
27
+ async def set(self, pol: Pol) -> None:
28
+ LOGGER.info(f"Setting insertion device polarisation to {pol.name}")
29
+ await self.polarisation().set(pol, timeout=MAXIMUM_MOVE_TIME)
30
+
31
+ async def locate(self) -> Location[Pol]:
32
+ """Return the current polarisation"""
33
+ setpoint, readback = await asyncio.gather(
34
+ self.polarisation_setpoint().get_value(), self.polarisation().get_value()
35
+ )
36
+ return Location(setpoint=setpoint, readback=readback)
@@ -1,13 +1,17 @@
1
+ import asyncio
1
2
  from enum import IntEnum
2
3
 
3
4
  from bluesky.protocols import Movable
4
5
  from ophyd_async.core import (
5
6
  DEFAULT_TIMEOUT,
6
7
  AsyncStatus,
8
+ DeviceMock,
9
+ DeviceVector,
7
10
  LazyMock,
8
11
  SignalR,
9
12
  SignalRW,
10
13
  StandardReadable,
14
+ default_mock_class,
11
15
  derived_signal_r,
12
16
  soft_signal_rw,
13
17
  )
@@ -22,6 +26,7 @@ from dodal.devices.oav.oav_parameters import (
22
26
  )
23
27
  from dodal.devices.oav.snapshots.snapshot import Snapshot
24
28
  from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
29
+ from dodal.log import LOGGER
25
30
 
26
31
 
27
32
  class Coords(IntEnum):
@@ -56,6 +61,35 @@ class NullZoomController(BaseZoomController):
56
61
  await self.level.set(value, wait=True)
57
62
 
58
63
 
64
+ class BeamCentreForZoom(StandardReadable):
65
+ """These PVs hold the beam centre on the OAV at each zoom level.
66
+
67
+ When the zoom level is changed the IOC will update the OAV overlay PVs to be at these positions."""
68
+
69
+ def __init__(
70
+ self, prefix: str, level_name_pv_suffix: str, centre_value_pv_suffix: str
71
+ ) -> None:
72
+ self.level_name = epics_signal_r(
73
+ str, f"{prefix}MP:SELECT.{level_name_pv_suffix}"
74
+ )
75
+ self.x_centre = epics_signal_rw(
76
+ float, f"{prefix}PBCX:VAL{centre_value_pv_suffix}"
77
+ )
78
+ self.y_centre = epics_signal_rw(
79
+ float, f"{prefix}PBCY:VAL{centre_value_pv_suffix}"
80
+ )
81
+ super().__init__()
82
+
83
+
84
+ class InstantMovingZoom(DeviceMock["ZoomController"]):
85
+ """Mock behaviour that instantly moves the zoom."""
86
+
87
+ async def connect(self, device: "ZoomController") -> None:
88
+ """Mock signals to do an instant move on setpoint write."""
89
+ device.DELAY_BETWEEN_MOTORS_AND_IMAGE_UPDATING_S = 0.001 # type:ignore
90
+
91
+
92
+ @default_mock_class(InstantMovingZoom)
59
93
  class ZoomController(BaseZoomController):
60
94
  """
61
95
  Device to control the zoom level. This should be set like
@@ -63,19 +97,49 @@ class ZoomController(BaseZoomController):
63
97
  oav.zoom_controller.set("1.0x")
64
98
 
65
99
  Note that changing the zoom may change the AD wiring on the associated OAV, as such
66
- you should wait on any zoom changs to finish before changing the OAV wiring.
100
+ you should wait on any zoom changes to finish before changing the OAV wiring.
67
101
  """
68
102
 
103
+ DELAY_BETWEEN_MOTORS_AND_IMAGE_UPDATING_S = 2
104
+
69
105
  def __init__(self, prefix: str, name: str = "") -> None:
70
106
  self.percentage = epics_signal_rw(float, f"{prefix}ZOOMPOSCMD")
71
107
 
72
108
  # Level is the string description of the zoom level e.g. "1.0x" or "1.0"
73
109
  self.level = epics_signal_rw(str, f"{prefix}MP:SELECT")
110
+
74
111
  super().__init__(name=name)
75
112
 
76
113
  @AsyncStatus.wrap
77
114
  async def set(self, value: str):
78
115
  await self.level.set(value, wait=True)
116
+ LOGGER.info(
117
+ "Waiting {self.DELAY_BETWEEN_MOTORS_AND_IMAGE_UPDATING_S} seconds for zoom to be noticeable"
118
+ )
119
+ await asyncio.sleep(self.DELAY_BETWEEN_MOTORS_AND_IMAGE_UPDATING_S)
120
+
121
+
122
+ class ZoomControllerWithBeamCentres(ZoomController):
123
+ def __init__(self, prefix: str, name: str = "") -> None:
124
+ level_to_centre_mapping = [
125
+ ("ZRST", "A"),
126
+ ("ONST", "B"),
127
+ ("TWST", "C"),
128
+ ("THST", "D"),
129
+ ("FRST", "E"),
130
+ ("FVST", "F"),
131
+ ("SXST", "G"),
132
+ ("SVST", "H"),
133
+ ]
134
+
135
+ self.beam_centres = DeviceVector(
136
+ {
137
+ i: BeamCentreForZoom(prefix, *level_to_centre_mapping[i])
138
+ for i in range(len(level_to_centre_mapping))
139
+ }
140
+ )
141
+
142
+ super().__init__(prefix, name)
79
143
 
80
144
 
81
145
  class OAV(StandardReadable):
@@ -118,6 +182,7 @@ class OAV(StandardReadable):
118
182
  self.zoom_controller = zoom_controller
119
183
 
120
184
  self.cam = Cam(f"{prefix}CAM:", name=name)
185
+
121
186
  with self.add_children_as_readables():
122
187
  self.grid_snapshot = SnapshotWithGrid(
123
188
  f"{prefix}{mjpeg_prefix}:", name, mjpg_x_size_pv, mjpg_y_size_pv
@@ -2,6 +2,7 @@ from collections.abc import Generator
2
2
  from enum import IntEnum
3
3
 
4
4
  import bluesky.plan_stubs as bps
5
+ import cv2
5
6
  import numpy as np
6
7
  from bluesky.utils import Msg
7
8
 
@@ -119,3 +120,19 @@ def wait_for_tip_to_be_found(
119
120
  raise PinNotFoundError(f"No pin found after {timeout} seconds")
120
121
 
121
122
  return Pixel((int(found_tip[0]), int(found_tip[1])))
123
+
124
+
125
+ def convert_to_gray_and_blur(data: cv2.typing.MatLike) -> cv2.typing.MatLike:
126
+ """
127
+ Preprocess the image array data (convert to grayscale and apply a gaussian blur)
128
+ Image is converted to grayscale (using a weighted mean as green contributes more to brightness)
129
+ as we aren't interested in data relating to colour. A blur is then applied to mitigate
130
+ errors due to rogue hot pixels.
131
+ """
132
+
133
+ # kernel size describes how many of the neighbouring pixels are used for the blur,
134
+ # higher kernal size means more of a blur effect
135
+ kernel_size = (7, 7)
136
+
137
+ gray_arr = cv2.cvtColor(data, cv2.COLOR_BGR2GRAY)
138
+ return cv2.GaussianBlur(gray_arr, kernel_size, 0)
dodal/devices/robot.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  from asyncio import FIRST_COMPLETED, CancelledError, Task, wait_for
3
3
  from dataclasses import dataclass
4
+ from enum import IntEnum
4
5
 
5
6
  from bluesky.protocols import Movable
6
7
  from ophyd_async.core import (
@@ -21,8 +22,8 @@ from ophyd_async.epics.core import (
21
22
 
22
23
  from dodal.log import LOGGER
23
24
 
24
- WAIT_FOR_OLD_PIN_MSG = "Waiting on old pin unloaded"
25
- WAIT_FOR_NEW_PIN_MSG = "Waiting on new pin loaded"
25
+ WAIT_FOR_BEAMLINE_DISABLE_MSG = "Waiting on beamline disable"
26
+ WAIT_FOR_BEAMLINE_ENABLE_MSG = "Waiting on beamline enable"
26
27
 
27
28
 
28
29
  class RobotLoadError(Exception):
@@ -43,11 +44,19 @@ class SampleLocation:
43
44
  pin: int
44
45
 
45
46
 
47
+ SAMPLE_LOCATION_EMPTY = SampleLocation(-1, -1)
48
+
49
+
46
50
  class PinMounted(StrictEnum):
47
51
  NO_PIN_MOUNTED = "No Pin Mounted"
48
52
  PIN_MOUNTED = "Pin Mounted"
49
53
 
50
54
 
55
+ class BeamlineStatus(IntEnum):
56
+ ENABLED = 0
57
+ DISABLED = 1
58
+
59
+
51
60
  class ErrorStatus(Device):
52
61
  def __init__(self, prefix: str) -> None:
53
62
  self.str = epics_signal_r(str, prefix + "_ERR_MSG")
@@ -61,7 +70,7 @@ class ErrorStatus(Device):
61
70
  raise RobotLoadError(int(error_code), error_string) from raise_from
62
71
 
63
72
 
64
- class BartRobot(StandardReadable, Movable[SampleLocation | None]):
73
+ class BartRobot(StandardReadable, Movable[SampleLocation]):
65
74
  """The sample changing robot."""
66
75
 
67
76
  # How long to wait for the robot if it is busy soaking/drying
@@ -86,6 +95,8 @@ class BartRobot(StandardReadable, Movable[SampleLocation | None]):
86
95
  self.current_puck = epics_signal_r(float, prefix + "CURRENT_PUCK_RBV")
87
96
  self.current_pin = epics_signal_r(float, prefix + "CURRENT_PIN_RBV")
88
97
 
98
+ self.beamline_disabled = epics_signal_r(int, prefix + "ROBOT_OP_16_BITS.B8")
99
+
89
100
  self.next_pin = epics_signal_rw_rbv(float, prefix + "NEXT_PIN")
90
101
  self.next_puck = epics_signal_rw_rbv(float, prefix + "NEXT_PUCK")
91
102
 
@@ -116,8 +127,8 @@ class BartRobot(StandardReadable, Movable[SampleLocation | None]):
116
127
  )
117
128
  super().__init__(name=name)
118
129
 
119
- async def pin_state_or_error(self, expected_state=PinMounted.PIN_MOUNTED):
120
- """This co-routine will finish when either the pin sensor reaches the specified
130
+ async def beamline_status_or_error(self, expected_state: BeamlineStatus):
131
+ """This co-routine will finish when either the beamline reaches the specified
121
132
  state or the robot gives an error (whichever happens first). In the case where
122
133
  there is an error a RobotLoadError error is raised.
123
134
  """
@@ -130,12 +141,12 @@ class BartRobot(StandardReadable, Movable[SampleLocation | None]):
130
141
  error_msg = await self.prog_error.str.get_value()
131
142
  raise RobotLoadError(error_code, error_msg)
132
143
 
133
- async def wfv():
134
- await wait_for_value(self.gonio_pin_sensor, expected_state, None)
144
+ async def wait_for_expected_state():
145
+ await wait_for_value(self.beamline_disabled, expected_state.value, None)
135
146
 
136
147
  tasks = [
137
148
  (Task(raise_if_error())),
138
- (Task(wfv())),
149
+ (Task(wait_for_expected_state())),
139
150
  ]
140
151
  try:
141
152
  finished, unfinished = await asyncio.wait(
@@ -171,31 +182,37 @@ class BartRobot(StandardReadable, Movable[SampleLocation | None]):
171
182
  set_and_wait_for_value(self.next_pin, sample_location.pin),
172
183
  )
173
184
  await self.load.trigger()
174
- if await self.gonio_pin_sensor.get_value() == PinMounted.PIN_MOUNTED:
175
- LOGGER.info(WAIT_FOR_OLD_PIN_MSG)
176
- await self.pin_state_or_error(PinMounted.NO_PIN_MOUNTED)
177
- LOGGER.info(WAIT_FOR_NEW_PIN_MSG)
185
+ await self._wait_for_beamline_enabled_after_load_or_unload()
178
186
 
179
- await self.pin_state_or_error()
187
+ async def _wait_for_beamline_enabled_after_load_or_unload(self):
188
+ if await self.beamline_disabled.get_value() == BeamlineStatus.ENABLED.value:
189
+ LOGGER.info(WAIT_FOR_BEAMLINE_DISABLE_MSG)
190
+ await self.beamline_status_or_error(BeamlineStatus.DISABLED)
191
+
192
+ LOGGER.info(WAIT_FOR_BEAMLINE_ENABLE_MSG)
193
+ await self.beamline_status_or_error(BeamlineStatus.ENABLED)
180
194
 
181
195
  @AsyncStatus.wrap
182
- async def set(self, value: SampleLocation | None):
196
+ async def set(self, value: SampleLocation):
183
197
  """
184
198
  Perform a sample load from the specified sample location
185
199
  Args:
186
- value: The pin and puck to load, or None to unload the sample.
200
+ value: The pin and puck to load, or SAMPLE_LOCATION_EMPTY to unload the sample.
187
201
  Raises:
188
- RobotLoadError if a timeout occurs, or if an error occurs loading the smaple.
202
+ RobotLoadError if a timeout occurs, or if an error occurs loading the sample.
189
203
  """
190
204
  try:
191
- if value is not None:
205
+ if value != SAMPLE_LOCATION_EMPTY:
192
206
  await wait_for(
193
207
  self._load_pin_and_puck(value),
194
208
  timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
195
209
  )
196
210
  else:
197
211
  await self.unload.trigger(timeout=self.LOAD_TIMEOUT)
198
- await wait_for_value(self.program_running, False, self.NOT_BUSY_TIMEOUT)
212
+ await wait_for(
213
+ self._wait_for_beamline_enabled_after_load_or_unload(),
214
+ timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
215
+ )
199
216
  except TimeoutError as e:
200
217
  await self.prog_error.raise_if_error(e)
201
218
  await self.controller_error.raise_if_error(e)
@@ -0,0 +1,38 @@
1
+ from typing import TypeVar
2
+
3
+ from bluesky.protocols import Movable
4
+ from ophyd_async.core import AsyncStatus, StandardReadable, StrictEnum, soft_signal_rw
5
+
6
+
7
+ class SelectedSource(StrictEnum):
8
+ SOURCE1 = "source1"
9
+ SOURCE2 = "source2"
10
+
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ def get_obj_from_selected_source(selected_source: SelectedSource, s1: T, s2: T) -> T:
16
+ """Util function that maps enum values for SelectedSource to two objects. It then
17
+ returns one of the objects that corrosponds to the selected_source value."""
18
+ match selected_source:
19
+ case SelectedSource.SOURCE1:
20
+ return s1
21
+ case SelectedSource.SOURCE2:
22
+ return s2
23
+
24
+
25
+ class SourceSelector(StandardReadable, Movable[SelectedSource]):
26
+ """Device that holds a selected_source signal enum of SelectedSource. Useful for
27
+ beamlines with multiple sources to coordinate which energy source or shutter to use."""
28
+
29
+ def __init__(self, name: str = ""):
30
+ with self.add_children_as_readables():
31
+ self.selected_source = soft_signal_rw(
32
+ SelectedSource, SelectedSource.SOURCE1
33
+ )
34
+ super().__init__(name)
35
+
36
+ @AsyncStatus.wrap
37
+ async def set(self, value: SelectedSource):
38
+ await self.selected_source.set(value)
@@ -7,11 +7,15 @@ from functools import partialmethod
7
7
  from bluesky.protocols import Movable
8
8
  from ophyd_async.core import (
9
9
  AsyncStatus,
10
+ DeviceMock,
10
11
  DeviceVector,
11
12
  SignalRW,
12
13
  StandardReadable,
13
14
  StrictEnum,
15
+ callback_on_mock_put,
16
+ default_mock_class,
14
17
  observe_value,
18
+ set_mock_value,
15
19
  )
16
20
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
17
21
 
@@ -88,6 +92,17 @@ class SoftInState(StrictEnum):
88
92
  NO = "No"
89
93
 
90
94
 
95
+ class InstantArmMock(DeviceMock["ArmingDevice"]):
96
+ async def connect(self, device: ArmingDevice) -> None:
97
+ callback_on_mock_put(
98
+ device.arm_set, lambda *_, **__: set_mock_value(device.armed, 1)
99
+ )
100
+ callback_on_mock_put(
101
+ device.disarm_set, lambda *_, **__: set_mock_value(device.armed, 0)
102
+ )
103
+
104
+
105
+ @default_mock_class(InstantArmMock)
91
106
  class ArmingDevice(StandardReadable, Movable[ArmDemand]):
92
107
  """A useful device that can abstract some of the logic of arming.
93
108
  Allows a user to just call arm.set(ArmDemand.ARM)"""
@@ -49,6 +49,7 @@ class ZebraTTLOutputs(ZebraMappingValidations):
49
49
  TTL_SHUTTER: int = Field(default=-1, ge=-1, le=4)
50
50
  TTL_XSPRESS3: int = Field(default=-1, ge=-1, le=4)
51
51
  TTL_PANDA: int = Field(default=-1, ge=-1, le=4)
52
+ TTL_JUNGFRAU: int = Field(default=-1, ge=-1, le=4)
52
53
 
53
54
 
54
55
  class ZebraSources(ZebraMappingValidations):
@@ -76,7 +76,6 @@ def set_cam_pvs(
76
76
  yield from bps.abs_set(
77
77
  eiger.drv.detector.frame_time, detector_params.exposure_time_s, group=group
78
78
  )
79
- yield from bps.abs_set(eiger.drv.detector.nexpi, 1, group=group)
80
79
 
81
80
  if wait:
82
81
  yield from bps.wait(group)
File without changes
File without changes