dls-dodal 1.55.0__py3-none-any.whl → 1.56.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 (101) hide show
  1. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/METADATA +3 -3
  2. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/RECORD +100 -86
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/b01_1.py +6 -13
  5. dodal/beamlines/b07.py +2 -1
  6. dodal/beamlines/b07_1.py +2 -1
  7. dodal/beamlines/b21.py +4 -24
  8. dodal/beamlines/i03.py +53 -53
  9. dodal/beamlines/i04.py +16 -38
  10. dodal/beamlines/i09.py +3 -2
  11. dodal/beamlines/i09_1.py +2 -1
  12. dodal/beamlines/i11.py +143 -0
  13. dodal/beamlines/i19_1.py +1 -0
  14. dodal/beamlines/i19_2.py +7 -0
  15. dodal/beamlines/i22.py +2 -2
  16. dodal/beamlines/i23.py +3 -3
  17. dodal/beamlines/i24.py +6 -14
  18. dodal/beamlines/p38.py +1 -0
  19. dodal/beamlines/p60.py +3 -2
  20. dodal/cli.py +11 -1
  21. dodal/common/__init__.py +4 -0
  22. dodal/common/beamlines/beamline_parameters.py +1 -1
  23. dodal/common/beamlines/beamline_utils.py +5 -1
  24. dodal/common/enums.py +19 -0
  25. dodal/common/watcher_utils.py +83 -0
  26. dodal/devices/aithre_lasershaping/laser_robot.py +4 -9
  27. dodal/devices/aperturescatterguard.py +52 -12
  28. dodal/devices/apple2_undulator.py +0 -1
  29. dodal/devices/attenuator/attenuator.py +3 -1
  30. dodal/devices/b16/detector.py +1 -10
  31. dodal/devices/backlight.py +8 -20
  32. dodal/devices/bimorph_mirror.py +4 -7
  33. dodal/devices/collimation_table.py +36 -0
  34. dodal/devices/controllers.py +21 -0
  35. dodal/devices/cryostream.py +97 -7
  36. dodal/devices/current_amplifiers/femto.py +1 -1
  37. dodal/devices/detector/detector_motion.py +1 -7
  38. dodal/devices/eiger.py +22 -8
  39. dodal/devices/eiger_odin.py +2 -0
  40. dodal/devices/electron_analyser/__init__.py +2 -1
  41. dodal/devices/electron_analyser/abstract/__init__.py +0 -1
  42. dodal/devices/electron_analyser/abstract/base_detector.py +3 -25
  43. dodal/devices/electron_analyser/abstract/base_driver_io.py +18 -9
  44. dodal/devices/electron_analyser/abstract/base_region.py +34 -3
  45. dodal/devices/electron_analyser/detector.py +24 -0
  46. dodal/devices/electron_analyser/enums.py +5 -0
  47. dodal/devices/electron_analyser/specs/detector.py +2 -1
  48. dodal/devices/electron_analyser/specs/driver_io.py +21 -26
  49. dodal/devices/electron_analyser/specs/region.py +1 -1
  50. dodal/devices/electron_analyser/util.py +20 -0
  51. dodal/devices/electron_analyser/vgscienta/__init__.py +3 -3
  52. dodal/devices/electron_analyser/vgscienta/detector.py +2 -1
  53. dodal/devices/electron_analyser/vgscienta/driver_io.py +24 -32
  54. dodal/devices/electron_analyser/vgscienta/enums.py +0 -8
  55. dodal/devices/electron_analyser/vgscienta/region.py +2 -31
  56. dodal/devices/eurotherm.py +126 -0
  57. dodal/devices/fluorescence_detector_motion.py +3 -10
  58. dodal/devices/focusing_mirror.py +1 -1
  59. dodal/devices/i03/undulator_dcm.py +0 -1
  60. dodal/devices/i09/enums.py +8 -8
  61. dodal/devices/i10/diagnostics.py +4 -4
  62. dodal/devices/i10/i10_apple2.py +3 -6
  63. dodal/devices/i11/cyberstar_blower.py +34 -0
  64. dodal/devices/i11/diff_stages.py +55 -0
  65. dodal/devices/i11/mythen.py +165 -0
  66. dodal/devices/i11/nx100robot.py +153 -0
  67. dodal/devices/i11/spinner.py +30 -0
  68. dodal/devices/i13_1/merlin_controller.py +4 -4
  69. dodal/devices/i19/diffractometer.py +34 -0
  70. dodal/devices/i19/shutter.py +11 -1
  71. dodal/devices/i22/dcm.py +1 -1
  72. dodal/devices/i22/fswitch.py +3 -12
  73. dodal/devices/i24/aperture.py +3 -3
  74. dodal/devices/i24/dcm.py +11 -15
  75. dodal/devices/i24/dual_backlight.py +11 -12
  76. dodal/devices/i24/pmac.py +8 -7
  77. dodal/devices/mx_phase1/beamstop.py +10 -11
  78. dodal/devices/oav/pin_image_recognition/__init__.py +0 -3
  79. dodal/devices/p60/enums.py +8 -8
  80. dodal/devices/p60/lab_xray_source.py +3 -2
  81. dodal/devices/pressure_jump_cell.py +77 -123
  82. dodal/devices/scintillator.py +76 -4
  83. dodal/devices/smargon.py +2 -2
  84. dodal/devices/synchrotron.py +1 -2
  85. dodal/devices/tetramm.py +13 -0
  86. dodal/devices/thawer.py +6 -11
  87. dodal/devices/undulator.py +3 -8
  88. dodal/devices/util/epics_util.py +1 -1
  89. dodal/devices/util/test_utils.py +19 -0
  90. dodal/devices/watsonmarlow323_pump.py +7 -7
  91. dodal/devices/xbpm_feedback.py +4 -6
  92. dodal/devices/xspress3/xspress3.py +0 -5
  93. dodal/devices/zocalo/zocalo_results.py +1 -3
  94. dodal/testing/__init__.py +0 -0
  95. dodal/testing/electron_analyser/__init__.py +6 -0
  96. dodal/testing/electron_analyser/device_factory.py +59 -0
  97. dodal/devices/CTAB.py +0 -41
  98. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/WHEEL +0 -0
  99. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/entry_points.txt +0 -0
  100. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/licenses/LICENSE +0 -0
  101. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,165 @@
1
+ import asyncio
2
+ from typing import Annotated as A
3
+
4
+ from ophyd_async.core import (
5
+ DetectorTrigger,
6
+ PathProvider,
7
+ SignalRW,
8
+ StrictEnum,
9
+ TriggerInfo,
10
+ )
11
+ from ophyd_async.epics.adcore import (
12
+ ADBaseController,
13
+ ADHDFWriter,
14
+ ADImageMode,
15
+ ADWriter,
16
+ AreaDetector,
17
+ NDArrayBaseIO,
18
+ )
19
+ from ophyd_async.epics.core import PvSuffix
20
+
21
+ from dodal.common.beamlines.device_helpers import DET_SUFFIX
22
+
23
+
24
+ class Mythen3TriggerMode(StrictEnum):
25
+ INTERNAL = "Internal"
26
+ EXTERNAL = "External"
27
+
28
+
29
+ class Mythen3ModeSetting(StrictEnum):
30
+ STANDARD = "standard"
31
+ FAST = "fast"
32
+ HIGHGAIN = "highgain"
33
+
34
+
35
+ class Mythen3DetectorState(StrictEnum):
36
+ """Default set of states of an Mythen3 driver."""
37
+
38
+ IDLE = "Idle"
39
+ ERROR = "Error"
40
+ WAITING = "Waiting"
41
+ FINISHED = "Finished"
42
+ TRASMITTING = "Transmitting"
43
+ RUNNING = "Running"
44
+ STOPPED = "Stopped"
45
+ INITIALIZING = "Initializing"
46
+ DISCONNECTED = "Disconnected"
47
+ ABORTED = "Aborted"
48
+
49
+
50
+ class Mythen3Driver(NDArrayBaseIO):
51
+ acquire_time: A[SignalRW[float], PvSuffix.rbv("AcquireTime")]
52
+ acquire_period: A[SignalRW[float], PvSuffix.rbv("AcquirePeriod")]
53
+ num_images: A[SignalRW[int], PvSuffix.rbv("NumImages")]
54
+ image_mode: A[SignalRW[ADImageMode], PvSuffix.rbv("ImageMode")]
55
+
56
+ # Non-specific PV's but with mythen3 specific values
57
+ detector_state: A[SignalRW[Mythen3DetectorState], PvSuffix("DetectorState_RBV")]
58
+ trigger_mode: A[SignalRW[Mythen3TriggerMode], PvSuffix.rbv("TriggerMode")]
59
+
60
+ # mythen3 specific PV's
61
+ mode_setting: A[SignalRW[Mythen3ModeSetting], PvSuffix.rbv("Setting")]
62
+ bit_depth: A[SignalRW[int], PvSuffix.rbv("BitDepth")]
63
+ beam_energy: A[SignalRW[int], PvSuffix.rbv("BeamEnergy")]
64
+ threshold: A[SignalRW[int], PvSuffix.rbv("Counter1Threshold")]
65
+ threshold2: A[SignalRW[int], PvSuffix.rbv("Counter2Threshold")]
66
+ threshold3: A[SignalRW[int], PvSuffix.rbv("Counter3Threshold")]
67
+ global_threshold: A[SignalRW[int], PvSuffix.rbv("ThresholdEnergy")]
68
+
69
+
70
+ """
71
+ Re. _DEADTIMES
72
+
73
+ see: https://doi.org/10.1107/S1600577525000438
74
+ Journal of Synchrotron Radiation, Volume 32, Part 2, March 2025, Pages 365-377
75
+ See Section 3.3, Table 2, Section 3.4 and Table 3
76
+
77
+ Note: Maximum frame rate of MYTHEN III is a function of the number of counters and of
78
+ the bit depth.
79
+
80
+ These numbers neglect possible limitations arising from the DAQ computing and network
81
+ system. Expect ~1 order of magnitude less
82
+
83
+ Our mythen3 is currently set up to use 3 counters, and 32bit depth. The deadtimes are
84
+ for 3 counters only. We can run faster with less counters/bit depth
85
+
86
+ The maximum frame rate of the detector scales both with the number of counters being
87
+ read out and with the bit depth, which can be configured to 24 (streamed out as 32 bit),
88
+ 16 or 8 bits see Table 3.
89
+
90
+ """
91
+ _DEADTIMES = {
92
+ 32: 1 / (30 * 1000), # values reported in frame rate (kHz)
93
+ 24: 1 / (40 * 1000),
94
+ 16: 1 / (60 * 1000),
95
+ 8: 1 / (120 * 1000),
96
+ }
97
+
98
+ _BIT_DEPTH = 24
99
+
100
+
101
+ class Mythen3Controller(ADBaseController):
102
+ """ADBaseController` for a Mythen3"""
103
+
104
+ def __init__(self, driver: Mythen3Driver):
105
+ self._driver = driver
106
+ super().__init__(driver=self._driver)
107
+
108
+ def get_deadtime(self, exposure: float | None) -> float:
109
+ return _DEADTIMES[_BIT_DEPTH]
110
+
111
+ async def prepare(self, trigger_info: TriggerInfo) -> None:
112
+ if (exposure := trigger_info.livetime) is not None:
113
+ await self._driver.acquire_time.set(exposure)
114
+
115
+ if trigger_info.trigger is DetectorTrigger.INTERNAL:
116
+ await self._driver.trigger_mode.set(Mythen3TriggerMode.INTERNAL)
117
+ elif trigger_info.trigger in {
118
+ DetectorTrigger.CONSTANT_GATE,
119
+ DetectorTrigger.EDGE_TRIGGER,
120
+ DetectorTrigger.VARIABLE_GATE,
121
+ }:
122
+ await self._driver.trigger_mode.set(Mythen3TriggerMode.EXTERNAL)
123
+ else:
124
+ raise ValueError(f"Mythen3 does not support {trigger_info.trigger}")
125
+
126
+ if trigger_info.total_number_of_exposures == 0:
127
+ image_mode = ADImageMode.CONTINUOUS
128
+ else:
129
+ image_mode = ADImageMode.MULTIPLE
130
+ await asyncio.gather(
131
+ self._driver.num_images.set(trigger_info.total_number_of_exposures),
132
+ self._driver.image_mode.set(image_mode),
133
+ )
134
+
135
+
136
+ class Mythen3(AreaDetector[Mythen3Controller]):
137
+ """
138
+ The detector may be configured for an external trigger on a GPIO port,
139
+ which must be done prior to preparing the detector
140
+ """
141
+
142
+ def __init__(
143
+ self,
144
+ prefix: str,
145
+ path_provider: PathProvider,
146
+ drv_suffix: str = DET_SUFFIX,
147
+ writer_cls: type[ADWriter] = ADHDFWriter,
148
+ fileio_suffix: str | None = "HDF:",
149
+ name: str = "",
150
+ ):
151
+ self.driver = Mythen3Driver(prefix + drv_suffix)
152
+ self.controller = Mythen3Controller(driver=self.driver)
153
+
154
+ self.writer = writer_cls.with_io(
155
+ prefix,
156
+ path_provider,
157
+ dataset_source=self.driver,
158
+ fileio_suffix=fileio_suffix,
159
+ )
160
+
161
+ super().__init__(
162
+ controller=self.controller,
163
+ writer=self.writer,
164
+ name=name,
165
+ ) # plugins=plugins # config_sigs=config_sigs
@@ -0,0 +1,153 @@
1
+ import asyncio
2
+ from enum import Enum
3
+
4
+ from bluesky.protocols import Locatable, Location, Pausable, Stoppable
5
+ from ophyd_async.core import (
6
+ AsyncStatus,
7
+ StandardReadable,
8
+ StrictEnum,
9
+ set_and_wait_for_value,
10
+ )
11
+ from ophyd_async.epics.core import epics_signal_rw, epics_signal_rw_rbv, epics_signal_x
12
+
13
+ from dodal.log import LOGGER
14
+
15
+
16
+ class RobotJobs(StrictEnum):
17
+ RECOVER = "RECOVER" # Recover from unknown state
18
+ PICK_CAROUSEL = "PICKC" # Pick a sample from the carousel.
19
+ PLACE_CAROUSEL = "PLACEC" # Place a sample onto the carousel
20
+ PICK_DIFFRACTOMETER = "PICKD" # Pick a sample from the diffractometer.
21
+ PLACE_DIFFRACTOMETER = "PLACED" # Place a sample onto the diffractometer.
22
+ GRIPO = "GRIPO"
23
+ GRIPC = "GRIPC"
24
+ TABLEIN = "TABLEIN"
25
+ TABLEOUT = "TABLEOUT"
26
+ UNLOAD = "UNLOAD"
27
+
28
+
29
+ class RobotSampleState(float, Enum):
30
+ CAROUSEL = 0.0 # Sample is on carousel
31
+ ONGRIP = 1.0 # Sample is on the gripper
32
+ DIFFRACTOMETER = 2.0 # Sample is on the diffractometer
33
+ UNKNOWN = 3.0
34
+
35
+
36
+ class NX100Robot(StandardReadable, Locatable[int], Stoppable, Pausable):
37
+ """
38
+ This is a Yaskawa Motoman that uses an NX100 controller. It consists of a robot arm
39
+ with a gripper and a carousel for sample handling. It can pick and place samples
40
+ from the carousel to the diffractometer and vice versa.
41
+
42
+ Has set, pause, resume, stop, locate, stage methods.
43
+ """
44
+
45
+ MAX_NUMBER_OF_SAMPLES = 200
46
+ MIN_NUMBER_OF_SAMPLES = 1
47
+
48
+ def __init__(self, prefix: str, name: str = ""):
49
+ self.start = epics_signal_x(prefix + "START")
50
+ self.hold = epics_signal_rw(bool, prefix + "HOLD")
51
+ self.job = epics_signal_rw(RobotJobs, prefix + "JOB")
52
+ self.servo_on = epics_signal_rw(bool, prefix + "SVON") # Servo on/off
53
+ self.err = epics_signal_rw(int, prefix + "ERR")
54
+
55
+ self.robot_sample_state = epics_signal_rw(float, prefix + "D010")
56
+ self.next_sample_position = epics_signal_rw_rbv(
57
+ int, prefix + "D011", read_suffix=":RBV"
58
+ )
59
+ self.current_sample_position = epics_signal_rw_rbv(
60
+ int, prefix + "D012", read_suffix=":RBV"
61
+ )
62
+ self.door_latch_state = epics_signal_rw(int, prefix + "NEEDRECOVER")
63
+
64
+ super().__init__(name=name)
65
+
66
+ async def recover(self):
67
+ await asyncio.gather(
68
+ self.start.trigger(),
69
+ set_and_wait_for_value(self.job, RobotJobs.RECOVER),
70
+ set_and_wait_for_value(self.err, False),
71
+ )
72
+
73
+ async def clear_sample(self, table_in: bool = True):
74
+ """clears the sample from the diffractometer and places it on the carousel.
75
+ if table_in is True, it will also move the sample holder to the table in position."""
76
+
77
+ sample_state = await self.robot_sample_state.get_value()
78
+ if sample_state == RobotSampleState.DIFFRACTOMETER:
79
+ await asyncio.gather(
80
+ set_and_wait_for_value(self.job, RobotJobs.PICK_DIFFRACTOMETER),
81
+ set_and_wait_for_value(self.job, RobotJobs.PLACE_CAROUSEL),
82
+ )
83
+ elif sample_state == RobotSampleState.ONGRIP:
84
+ await set_and_wait_for_value(self.job, RobotJobs.PLACE_CAROUSEL)
85
+ elif sample_state == RobotSampleState.CAROUSEL:
86
+ pass
87
+ elif sample_state == RobotSampleState.UNKNOWN:
88
+ LOGGER.error("UNKNOWN sample state from robot, exit")
89
+ else:
90
+ raise ValueError(f"Unknown sample state: {sample_state}")
91
+
92
+ if table_in:
93
+ await set_and_wait_for_value(self.job, RobotJobs.TABLEIN)
94
+
95
+ LOGGER.info("Sample cleared from diffractometer")
96
+
97
+ async def load_sample(self, sample_location: int):
98
+ """Loads a sample from the carousel to the diffractometer."""
99
+ sample_state = await self.robot_sample_state.get_value()
100
+ if sample_state == RobotSampleState.CAROUSEL:
101
+ await set_and_wait_for_value(self.job, RobotJobs.PICK_CAROUSEL)
102
+ await set_and_wait_for_value(self.job, RobotJobs.PLACE_DIFFRACTOMETER)
103
+ elif sample_state == RobotSampleState.ONGRIP:
104
+ await set_and_wait_for_value(self.job, RobotJobs.PLACE_DIFFRACTOMETER)
105
+ elif sample_state == RobotSampleState.DIFFRACTOMETER:
106
+ pass
107
+ elif sample_state == RobotSampleState.UNKNOWN:
108
+ LOGGER.warning(f"No sample at sample holder position {sample_location}")
109
+ LOGGER.error("UNKNOWN sample state from robot, exit")
110
+ else:
111
+ raise ValueError(f"Unknown sample state: {sample_state}")
112
+
113
+ @AsyncStatus.wrap
114
+ async def set(self, sample_location: int) -> None:
115
+ if (
116
+ sample_location < self.MIN_NUMBER_OF_SAMPLES
117
+ or sample_location > self.MAX_NUMBER_OF_SAMPLES
118
+ ):
119
+ raise ValueError(
120
+ f"Sample location must be between {self.MIN_NUMBER_OF_SAMPLES} and {self.MAX_NUMBER_OF_SAMPLES}, got {sample_location}"
121
+ )
122
+ if await self.current_sample_position.get_value() == sample_location:
123
+ LOGGER.info(f"Robot already at position {sample_location}")
124
+ else:
125
+ await self.clear_sample(table_in=False)
126
+ await self.next_sample_position.set(sample_location, wait=True)
127
+ await self.load_sample(sample_location)
128
+ await self.current_sample_position.set(sample_location)
129
+
130
+ async def pause(self):
131
+ await set_and_wait_for_value(self.hold, True)
132
+
133
+ async def resume(self):
134
+ await set_and_wait_for_value(self.hold, False)
135
+
136
+ async def stop(self, success: bool = True):
137
+ await set_and_wait_for_value(self.hold, True)
138
+ if not success:
139
+ await set_and_wait_for_value(self.hold, False)
140
+ await self.clear_sample()
141
+
142
+ @AsyncStatus.wrap
143
+ async def stage(self) -> None:
144
+ """Set up the device for acquisition."""
145
+ await asyncio.gather(
146
+ set_and_wait_for_value(self.servo_on, True),
147
+ set_and_wait_for_value(self.hold, False),
148
+ self.start.trigger(),
149
+ )
150
+
151
+ async def locate(self) -> Location[int]:
152
+ location = await self.current_sample_position.locate()
153
+ return location
@@ -0,0 +1,30 @@
1
+ from bluesky.protocols import Pausable
2
+ from ophyd_async.core import (
3
+ EnabledDisabled,
4
+ StandardReadable,
5
+ set_and_wait_for_value,
6
+ )
7
+ from ophyd_async.epics.core import epics_signal_rw
8
+
9
+
10
+ class Spinner(StandardReadable, Pausable):
11
+ """This is a simple sample spinner, that has enable and speed (%)"""
12
+
13
+ def __init__(
14
+ self,
15
+ prefix: str,
16
+ name: str = "",
17
+ disable_suffix: str = "SPIN:DISABLE",
18
+ speed_suffix: str = "SPIN:SPEED",
19
+ ):
20
+ with self.add_children_as_readables():
21
+ self.enable = epics_signal_rw(EnabledDisabled, prefix + disable_suffix)
22
+ self.speed = epics_signal_rw(float, prefix + speed_suffix)
23
+
24
+ super().__init__(name=name)
25
+
26
+ async def pause(self):
27
+ await set_and_wait_for_value(self.enable, EnabledDisabled.DISABLED)
28
+
29
+ async def resume(self):
30
+ await set_and_wait_for_value(self.enable, EnabledDisabled.ENABLED)
@@ -8,15 +8,16 @@ from ophyd_async.core import (
8
8
  )
9
9
  from ophyd_async.epics.adcore import (
10
10
  DEFAULT_GOOD_STATES,
11
- ADBaseController,
12
11
  ADBaseIO,
13
12
  ADImageMode,
14
13
  ADState,
15
14
  )
16
15
  from ophyd_async.epics.core import stop_busy_record
17
16
 
17
+ from dodal.devices.controllers import ConstantDeadTimeController
18
18
 
19
- class MerlinController(ADBaseController):
19
+
20
+ class MerlinController(ConstantDeadTimeController):
20
21
  def __init__(
21
22
  self,
22
23
  driver: ADBaseIO,
@@ -29,8 +30,7 @@ class MerlinController(ADBaseController):
29
30
  for drv_child in self.driver.children():
30
31
  logging.debug(drv_child)
31
32
 
32
- def get_deadtime(self, exposure: float | None) -> float:
33
- return 0.002
33
+ super().__init__(driver, 0.002)
34
34
 
35
35
  async def prepare(self, trigger_info: TriggerInfo):
36
36
  self.frame_timeout = (
@@ -0,0 +1,34 @@
1
+ from ophyd_async.epics.motor import Motor
2
+
3
+ from dodal.devices.motors import Stage, XYZStage
4
+
5
+ CIRC = "-MO-CIRC-02:" # for phi, kappa, omega, 2theta and det_z
6
+ SAMP = "-MO-SAMP-02:" # for x,y,z
7
+
8
+
9
+ class DetectorMotion(Stage):
10
+ def __init__(self, prefix: str, name: str = ""):
11
+ with self.add_children_as_readables():
12
+ self.det_z = Motor(f"{prefix}DET")
13
+ self.two_theta = Motor(f"{prefix}2THETA")
14
+ super().__init__(name=name)
15
+
16
+
17
+ # Collision model needs to be included
18
+ # See https://github.com/DiamondLightSource/dodal/issues/1073
19
+ class FourCircleDiffractometer(XYZStage):
20
+ """Newport 4-circle diffractometer device."""
21
+
22
+ def __init__(
23
+ self,
24
+ prefix: str,
25
+ name: str = "",
26
+ circ_infix: str = CIRC,
27
+ samp_infix: str = SAMP,
28
+ ):
29
+ with self.add_children_as_readables():
30
+ self.phi = Motor(f"{prefix}{circ_infix}SAM:PHI")
31
+ self.omega = Motor(f"{prefix}{circ_infix}SAM:OMEGA")
32
+ self.kappa = Motor(f"{prefix}{circ_infix}SAM:KAPPA")
33
+ self.det_stage = DetectorMotion(f"{prefix}{circ_infix}SAM:", name)
34
+ super().__init__(f"{prefix}{samp_infix}SAM:", name)
@@ -22,10 +22,19 @@ class AccessControlledShutter(OpticsBlueAPIDevice):
22
22
  https://github.com/DiamondLightSource/i19-bluesky/issues/30.
23
23
  """
24
24
 
25
- def __init__(self, prefix: str, hutch: HutchState, name: str = "") -> None:
25
+ def __init__(
26
+ self,
27
+ prefix: str,
28
+ hutch: HutchState,
29
+ instrument_session: str = "",
30
+ name: str = "",
31
+ ) -> None:
32
+ # For instrument session addition to request parameters
33
+ # see https://github.com/DiamondLightSource/blueapi/issues/1187
26
34
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
27
35
  self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
28
36
  self.hutch_request = hutch
37
+ self.instrument_session = instrument_session
29
38
  super().__init__(name)
30
39
 
31
40
  @AsyncStatus.wrap
@@ -37,5 +46,6 @@ class AccessControlledShutter(OpticsBlueAPIDevice):
37
46
  "access_device": ACCESS_DEVICE_NAME,
38
47
  "shutter_demand": value.value,
39
48
  },
49
+ "instrument_session": self.instrument_session,
40
50
  }
41
51
  await super().set(REQUEST_PARAMS)
dodal/devices/i22/dcm.py CHANGED
@@ -38,7 +38,7 @@ class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
38
38
  temperature_prefix: str,
39
39
  crystal_1_metadata: CrystalMetadata,
40
40
  crystal_2_metadata: CrystalMetadata,
41
- prefix: str = "",
41
+ prefix: str,
42
42
  name: str = "",
43
43
  ) -> None:
44
44
  with self.add_children_as_readables():
@@ -7,20 +7,11 @@ from ophyd_async.core import (
7
7
  DeviceVector,
8
8
  StandardReadable,
9
9
  StandardReadableFormat,
10
- StrictEnum,
11
10
  soft_signal_r_and_setter,
12
11
  )
13
12
  from ophyd_async.epics.core import epics_signal_r
14
13
 
15
-
16
- class FilterState(StrictEnum):
17
- """
18
- Note that the in/out here refers to the internal rocker
19
- position so a PV value of IN implies a filter OUT of beam
20
- """
21
-
22
- IN_BEAM = "OUT"
23
- OUT_BEAM = "IN"
14
+ from dodal.common.enums import InOutUpper
24
15
 
25
16
 
26
17
  class FSwitch(StandardReadable):
@@ -50,7 +41,7 @@ class FSwitch(StandardReadable):
50
41
  ) -> None:
51
42
  self.filters = DeviceVector(
52
43
  {
53
- i: epics_signal_r(FilterState, f"{prefix}FILTER-{i:03}:STATUS_RBV")
44
+ i: epics_signal_r(InOutUpper, f"{prefix}FILTER-{i:03}:STATUS_RBV")
54
45
  for i in range(FSwitch.NUM_FILTERS)
55
46
  }
56
47
  )
@@ -91,7 +82,7 @@ class FSwitch(StandardReadable):
91
82
  result = await asyncio.gather(
92
83
  *(filter.get_value() for filter in self.filters.values())
93
84
  )
94
- num_in = sum(r.value == FilterState.IN_BEAM for r in result)
85
+ num_in = sum(r.value == InOutUpper.IN for r in result)
95
86
  default_reading = await super().read()
96
87
  return {
97
88
  FSwitch.NUM_LENSES_FIELD_NAME: Reading(value=num_in, timestamp=time.time()),
@@ -1,12 +1,12 @@
1
- from ophyd_async.core import StrictEnum
1
+ from ophyd_async.core import InOut, StrictEnum
2
2
  from ophyd_async.epics.core import epics_signal_rw
3
3
 
4
4
  from dodal.devices.motors import XYStage
5
5
 
6
6
 
7
7
  class AperturePositions(StrictEnum):
8
- IN = "In"
9
- OUT = "Out"
8
+ IN = InOut.IN.value
9
+ OUT = InOut.OUT.value
10
10
  ROBOT = "Robot"
11
11
  MANUAL = "Manual Mounting"
12
12
 
dodal/devices/i24/dcm.py CHANGED
@@ -12,22 +12,18 @@ class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
12
12
  A double crystal monocromator device, used to select the beam energy.
13
13
  """
14
14
 
15
- def __init__(self, prefix: str, name: str = "") -> None:
15
+ def __init__(self, prefix: str, motion_prefix: str, name: str = "") -> None:
16
16
  with self.add_children_as_readables():
17
17
  # Temperatures
18
- self.xtal1_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-1")
19
- self.xtal1_heater_temp = epics_signal_r(
20
- float, prefix + "-DI-DCM-01:PT100-2"
21
- )
22
- self.xtal2_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-4")
23
- self.xtal2_heater_temp = epics_signal_r(
24
- float, prefix + "-DI-DCM-01:PT100-5"
25
- )
18
+ self.xtal1_temp = epics_signal_r(float, prefix + "PT100-1")
19
+ self.xtal1_heater_temp = epics_signal_r(float, prefix + "PT100-2")
20
+ self.xtal2_temp = epics_signal_r(float, prefix + "PT100-4")
21
+ self.xtal2_heater_temp = epics_signal_r(float, prefix + "PT100-5")
26
22
 
27
- self.roll_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-3")
28
- self.pitch_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-6")
29
- self.backplate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-7")
30
- self.b1_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-7")
31
- self.gap_temp = epics_signal_r(float, prefix + "-DI-DCM-01:TC-1")
23
+ self.roll_plate_temp = epics_signal_r(float, prefix + "PT100-3")
24
+ self.pitch_plate_temp = epics_signal_r(float, prefix + "PT100-6")
25
+ self.backplate_temp = epics_signal_r(float, prefix + "PT100-7")
26
+ self.b1_plate_temp = epics_signal_r(float, prefix + "PT100-7")
27
+ self.gap_temp = epics_signal_r(float, prefix + "TC-1")
32
28
 
33
- super().__init__(prefix + "-MO-DCM-01:", RollCrystal, PitchAndRollCrystal, name)
29
+ super().__init__(motion_prefix, RollCrystal, PitchAndRollCrystal, name)
@@ -1,21 +1,18 @@
1
- from ophyd_async.core import AsyncStatus, StandardReadable, StrictEnum
1
+ from ophyd_async.core import AsyncStatus, InOut, StandardReadable, StrictEnum
2
2
  from ophyd_async.epics.core import epics_signal_rw
3
3
 
4
+ from dodal.common.enums import OnOffUpper
5
+
4
6
 
5
7
  class BacklightPositions(StrictEnum):
6
- OUT = "Out"
7
- IN = "In"
8
+ OUT = InOut.OUT.value
9
+ IN = InOut.IN.value
8
10
  LOAD_CHECK = "LoadCheck"
9
11
  OAV2 = "OAV2"
10
12
  DIODE = "Diode"
11
13
  WHITE_IN = "White In"
12
14
 
13
15
 
14
- class LEDStatus(StrictEnum):
15
- OFF = "OFF"
16
- ON = "ON"
17
-
18
-
19
16
  class BacklightPositioner(StandardReadable):
20
17
  """Device to control the backlight position."""
21
18
 
@@ -45,16 +42,18 @@ class DualBacklight(StandardReadable):
45
42
  """
46
43
 
47
44
  def __init__(self, prefix: str, name: str = "") -> None:
48
- self.backlight_state = epics_signal_rw(LEDStatus, prefix + "-DI-LED-01:TOGGLE")
45
+ self.backlight_state = epics_signal_rw(OnOffUpper, prefix + "-DI-LED-01:TOGGLE")
49
46
  self.backlight_position = BacklightPositioner(prefix + "-MO-BL-01:", name)
50
47
 
51
- self.frontlight_state = epics_signal_rw(LEDStatus, prefix + "-DI-LED-02:TOGGLE")
48
+ self.frontlight_state = epics_signal_rw(
49
+ OnOffUpper, prefix + "-DI-LED-02:TOGGLE"
50
+ )
52
51
  super().__init__(name)
53
52
 
54
53
  @AsyncStatus.wrap
55
54
  async def set(self, value: BacklightPositions):
56
55
  await self.backlight_position.set(value)
57
56
  if value == BacklightPositions.OUT:
58
- await self.backlight_state.set(LEDStatus.OFF, wait=True)
57
+ await self.backlight_state.set(OnOffUpper.OFF, wait=True)
59
58
  else:
60
- await self.backlight_state.set(LEDStatus.ON, wait=True)
59
+ await self.backlight_state.set(OnOffUpper.ON, wait=True)
dodal/devices/i24/pmac.py CHANGED
@@ -17,8 +17,9 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
17
17
 
18
18
  from dodal.devices.motors import XYZStage
19
19
 
20
- HOME_STR = r"\#1hmz\#2hmz\#3hmz" # Command to home the PMAC motors
20
+ HOME_STR = r"\#5hmz\#6hmz\#7hmz" # Command to home the PMAC motors
21
21
  ZERO_STR = "!x0y0z0" # Command to blend any ongoing move into new position
22
+ CS_STR = "&2"
22
23
 
23
24
 
24
25
  class ScanState(IntEnum):
@@ -196,12 +197,12 @@ class PMAC(XYZStage):
196
197
  """Device to control the chip stage on I24."""
197
198
 
198
199
  def __init__(self, prefix: str, name: str = "") -> None:
199
- self.pmac_string = epics_signal_rw(str, prefix + "PMAC_STRING")
200
+ self.pmac_string = epics_signal_rw(str, f"{prefix}-MO-IOC-13:PMAC:console")
200
201
  self.home = PMACStringMove(
201
202
  self.pmac_string,
202
- HOME_STR,
203
+ f"{CS_STR}{HOME_STR}",
203
204
  )
204
- self.to_xyz_zero = PMACStringMove(self.pmac_string, ZERO_STR)
205
+ self.to_xyz_zero = PMACStringMove(self.pmac_string, f"{CS_STR}{ZERO_STR}")
205
206
 
206
207
  self.laser = PMACStringLaser(self.pmac_string)
207
208
 
@@ -211,8 +212,8 @@ class PMAC(XYZStage):
211
212
 
212
213
  # These next signals are readback values on PVARS which are set by the motion
213
214
  # program.
214
- self.scanstatus = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2401")
215
- self.counter = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2402")
215
+ self.scanstatus = epics_signal_r(float, f"{prefix}-MO-STEP-13:pmac:read:P2401")
216
+ self.counter = epics_signal_r(float, f"{prefix}-MO-STEP-13:pmac:read:P2402")
216
217
 
217
218
  # A couple of soft signals for running a collection: program number to send to
218
219
  # the PMAC_STRING and expected collection time.
@@ -228,4 +229,4 @@ class PMAC(XYZStage):
228
229
  )
229
230
  self.abort_program = ProgramAbort(self.pmac_string, self.scanstatus)
230
231
 
231
- super().__init__(prefix, name)
232
+ super().__init__(f"{prefix}-MO-CHIP-01:", name)
@@ -18,9 +18,6 @@ class BeamstopPositions(StrictEnum):
18
18
  robot load however all 3 resolution positions are the same. We also
19
19
  do not use the robot load position in Hyperion.
20
20
 
21
- Until we support moving the beamstop it is only necessary to check whether the
22
- beamstop is in beam or not.
23
-
24
21
  See Also:
25
22
  https://github.com/DiamondLightSource/mx-bluesky/issues/484
26
23
 
@@ -86,11 +83,13 @@ class Beamstop(StandardReadable):
86
83
  return BeamstopPositions.UNKNOWN
87
84
 
88
85
  async def _set_selected_position(self, position: BeamstopPositions) -> None:
89
- if position == BeamstopPositions.DATA_COLLECTION:
90
- await asyncio.gather(
91
- self.x_mm.set(self._in_beam_xyz_mm[0]),
92
- self.y_mm.set(self._in_beam_xyz_mm[1]),
93
- self.z_mm.set(self._in_beam_xyz_mm[2]),
94
- )
95
- elif position == BeamstopPositions.UNKNOWN:
96
- raise ValueError(f"Cannot set beamstop to position {position}")
86
+ match position:
87
+ case BeamstopPositions.DATA_COLLECTION:
88
+ # Move z first as it could be under the table
89
+ await self.z_mm.set(self._in_beam_xyz_mm[2])
90
+ await asyncio.gather(
91
+ self.x_mm.set(self._in_beam_xyz_mm[0]),
92
+ self.y_mm.set(self._in_beam_xyz_mm[1]),
93
+ )
94
+ case _:
95
+ raise ValueError(f"Cannot set beamstop to position {position}")