dls-dodal 1.43.0__py3-none-any.whl → 1.44.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 (45) hide show
  1. {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info}/METADATA +4 -3
  2. {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info}/RECORD +42 -34
  3. {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/b01_1.py +8 -0
  6. dodal/beamlines/i03.py +11 -0
  7. dodal/beamlines/i13_1.py +22 -48
  8. dodal/beamlines/i19_1.py +16 -5
  9. dodal/beamlines/i19_2.py +12 -3
  10. dodal/beamlines/i19_optics.py +4 -2
  11. dodal/common/data_util.py +20 -0
  12. dodal/common/signal_utils.py +43 -4
  13. dodal/devices/aperturescatterguard.py +3 -3
  14. dodal/devices/baton.py +17 -0
  15. dodal/devices/current_amplifiers/current_amplifier.py +1 -6
  16. dodal/devices/current_amplifiers/current_amplifier_detector.py +2 -2
  17. dodal/devices/current_amplifiers/femto.py +0 -5
  18. dodal/devices/current_amplifiers/sr570.py +0 -5
  19. dodal/devices/electron_analyser/__init__.py +0 -0
  20. dodal/devices/electron_analyser/base_region.py +64 -0
  21. dodal/devices/electron_analyser/specs/__init__.py +0 -0
  22. dodal/devices/electron_analyser/specs/specs_region.py +24 -0
  23. dodal/devices/electron_analyser/vgscienta/__init__.py +0 -0
  24. dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +77 -0
  25. dodal/devices/fast_grid_scan.py +2 -2
  26. dodal/devices/i03/beamstop.py +2 -2
  27. dodal/devices/i13_1/merlin.py +1 -2
  28. dodal/devices/i13_1/merlin_controller.py +12 -8
  29. dodal/devices/i19/beamstop.py +30 -0
  30. dodal/devices/i19/hutch_access.py +2 -0
  31. dodal/devices/i19/shutter.py +52 -30
  32. dodal/devices/i22/nxsas.py +1 -3
  33. dodal/devices/i24/focus_mirrors.py +3 -3
  34. dodal/devices/i24/pilatus_metadata.py +2 -2
  35. dodal/devices/oav/oav_detector.py +7 -9
  36. dodal/devices/oav/snapshots/snapshot.py +21 -0
  37. dodal/devices/oav/snapshots/snapshot_image_processing.py +74 -0
  38. dodal/plan_stubs/motor_utils.py +10 -12
  39. dodal/utils.py +0 -7
  40. dodal/devices/i13_1/merlin_io.py +0 -17
  41. dodal/devices/oav/microns_for_zoom_levels.json +0 -55
  42. dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +0 -64
  43. {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info}/entry_points.txt +0 -0
  44. {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info/licenses}/LICENSE +0 -0
  45. {dls_dodal-1.43.0.dist-info → dls_dodal-1.44.0.dist-info}/top_level.txt +0 -0
@@ -23,7 +23,6 @@ class CurrentAmp(ABC, StandardReadable, Movable):
23
23
  super().__init__(name)
24
24
 
25
25
  @abstractmethod
26
- @AsyncStatus.wrap
27
26
  async def increase_gain(self, value: int = 1) -> None:
28
27
  """Increase gain, increment by 1 by default.
29
28
 
@@ -31,7 +30,6 @@ class CurrentAmp(ABC, StandardReadable, Movable):
31
30
  bool: True if success.
32
31
  """
33
32
 
34
- @AsyncStatus.wrap
35
33
  @abstractmethod
36
34
  async def decrease_gain(self, value: int = 1) -> None:
37
35
  """Decrease gain, decrement by 1 by default.
@@ -40,21 +38,18 @@ class CurrentAmp(ABC, StandardReadable, Movable):
40
38
  bool: True if success.
41
39
  """
42
40
 
43
- @AsyncStatus.wrap
44
41
  @abstractmethod
45
- async def get_gain(self) -> type[Enum]:
42
+ async def get_gain(self) -> Enum:
46
43
  """Get the current gain setting
47
44
 
48
45
  Returns:
49
46
  Enum: The member name of the current gain setting in gain_conversion_table.
50
47
  """
51
48
 
52
- @AsyncStatus.wrap
53
49
  @abstractmethod
54
50
  async def get_upperlimit(self) -> float:
55
51
  """Get the upper limit of the current amplifier"""
56
52
 
57
- @AsyncStatus.wrap
58
53
  @abstractmethod
59
54
  async def get_lowerlimit(self) -> float:
60
55
  """Get the lower limit of the current amplifier"""
@@ -92,8 +92,8 @@ class CurrentAmpDet(StandardReadable, Preparable):
92
92
  self.current_amp().get_gain(),
93
93
  self.counter().get_voltage_per_sec(),
94
94
  )
95
- correction_factor = current_gain.value
96
- corrected_current = voltage_per_sec / correction_factor
95
+ assert isinstance(current_gain.value, float)
96
+ corrected_current = voltage_per_sec / current_gain.value
97
97
  return corrected_current
98
98
 
99
99
  @AsyncStatus.wrap
@@ -114,7 +114,6 @@ class FemtoDDPCA(CurrentAmp):
114
114
  # wait for current amplifier's bandpass filter to settle.
115
115
  await asyncio.sleep(self.raise_timetable[SEN_setting].value)
116
116
 
117
- @AsyncStatus.wrap
118
117
  async def increase_gain(self, value: int = 1) -> None:
119
118
  current_gain = int((await self.get_gain()).name.split("_")[-1])
120
119
  current_gain += value
@@ -122,7 +121,6 @@ class FemtoDDPCA(CurrentAmp):
122
121
  raise ValueError("Gain at max value")
123
122
  await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
124
123
 
125
- @AsyncStatus.wrap
126
124
  async def decrease_gain(self, value: int = 1) -> None:
127
125
  current_gain = int((await self.get_gain()).name.split("_")[-1])
128
126
  current_gain -= value
@@ -130,14 +128,11 @@ class FemtoDDPCA(CurrentAmp):
130
128
  raise ValueError("Gain at min value")
131
129
  await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
132
130
 
133
- @AsyncStatus.wrap
134
131
  async def get_gain(self) -> Enum:
135
132
  return self.gain_conversion_table[(await self.gain.get_value()).name]
136
133
 
137
- @AsyncStatus.wrap
138
134
  async def get_upperlimit(self) -> float:
139
135
  return self.upperlimit
140
136
 
141
- @AsyncStatus.wrap
142
137
  async def get_lowerlimit(self) -> float:
143
138
  return self.lowerlimit
@@ -178,7 +178,6 @@ class SR570(CurrentAmp):
178
178
  )
179
179
  await asyncio.sleep(self.raise_timetable[coarse_gain.name].value)
180
180
 
181
- @AsyncStatus.wrap
182
181
  async def increase_gain(self, value=3) -> None:
183
182
  current_gain = int((await self.get_gain()).name.split("_")[-1])
184
183
  current_gain += value
@@ -189,7 +188,6 @@ class SR570(CurrentAmp):
189
188
  raise ValueError("Gain at max value")
190
189
  await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
191
190
 
192
- @AsyncStatus.wrap
193
191
  async def decrease_gain(self, value=3) -> None:
194
192
  current_gain = int((await self.get_gain()).name.split("_")[-1])
195
193
  current_gain -= value
@@ -198,17 +196,14 @@ class SR570(CurrentAmp):
198
196
  raise ValueError("Gain at min value")
199
197
  await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
200
198
 
201
- @AsyncStatus.wrap
202
199
  async def get_gain(self) -> Enum:
203
200
  result = await asyncio.gather(
204
201
  self.coarse_gain.get_value(), self.fine_gain.get_value()
205
202
  )
206
203
  return self.gain_conversion_table[self.combined_table(result).name]
207
204
 
208
- @AsyncStatus.wrap
209
205
  async def get_upperlimit(self) -> float:
210
206
  return self.upperlimit
211
207
 
212
- @AsyncStatus.wrap
213
208
  async def get_lowerlimit(self) -> float:
214
209
  return self.lowerlimit
File without changes
@@ -0,0 +1,64 @@
1
+ from enum import Enum
2
+ from typing import Any, Generic, TypeVar
3
+
4
+ from pydantic import BaseModel, Field, model_validator
5
+
6
+
7
+ class EnergyMode(str, Enum):
8
+ KINETIC = "Kinetic"
9
+ BINDING = "Binding"
10
+
11
+
12
+ class BaseRegion(BaseModel):
13
+ """
14
+ Generic region model that holds the data. Specialised region models should inherit
15
+ this to extend functionality.
16
+ """
17
+
18
+ name: str = "New_region"
19
+ enabled: bool = False
20
+ slices: int = 1
21
+ iterations: int = 1
22
+ # These ones we need subclasses to provide default values
23
+ lensMode: str
24
+ passEnergy: int | float
25
+ acquisitionMode: str
26
+ lowEnergy: float
27
+ highEnergy: float
28
+ stepTime: float
29
+ energyStep: float
30
+ energyMode: EnergyMode = EnergyMode.KINETIC
31
+
32
+ @model_validator(mode="before")
33
+ @classmethod
34
+ def check_energy_mode(cls, data: Any) -> Any:
35
+ if isinstance(data, dict):
36
+ # convert bindingEnergy to energyMode to make base region more generic
37
+ if "bindingEnergy" in data:
38
+ is_binding_energy = data["bindingEnergy"]
39
+ del data["bindingEnergy"]
40
+ data["energyMode"] = (
41
+ EnergyMode.BINDING if is_binding_energy else EnergyMode.KINETIC
42
+ )
43
+ return data
44
+
45
+
46
+ TBaseRegion = TypeVar("TBaseRegion", bound=BaseRegion)
47
+
48
+
49
+ class BaseSequence(BaseModel, Generic[TBaseRegion]):
50
+ """
51
+ Generic sequence model that holds the list of region data. Specialised sequence models
52
+ should inherit this to extend functionality.
53
+ """
54
+
55
+ regions: list[TBaseRegion] = Field(default_factory=lambda: [])
56
+
57
+ def get_enabled_regions(self) -> list[BaseRegion]:
58
+ return [r for r in self.regions if r.enabled]
59
+
60
+ def get_region_names(self) -> list[str]:
61
+ return [r.name for r in self.regions]
62
+
63
+ def get_enabled_region_names(self) -> list[str]:
64
+ return [r.name for r in self.get_enabled_regions()]
File without changes
@@ -0,0 +1,24 @@
1
+ from pydantic import Field
2
+
3
+ from dodal.devices.electron_analyser.base_region import BaseRegion, BaseSequence
4
+
5
+
6
+ class SpecsRegion(BaseRegion):
7
+ # Override base class with defaults
8
+ lensMode: str = "SmallArea"
9
+ passEnergy: int | float = 5.0
10
+ acquisitionMode: str = "Fixed Transmission"
11
+ lowEnergy: float = Field(default=800, alias="startEnergy")
12
+ highEnergy: float = Field(default=850, alias="endEnergy")
13
+ stepTime: float = Field(default=1.0, alias="exposureTime")
14
+ energyStep: float = Field(default=0.1, alias="stepEnergy")
15
+ # Specific to this class
16
+ values: float = 1
17
+ centreEnergy: float = 0
18
+ psuMode: str = "1.5keV"
19
+ acquisitionMode: str = ""
20
+ estimatedTimeInMs: float = 0
21
+
22
+
23
+ class SpecsSequence(BaseSequence):
24
+ regions: list[SpecsRegion] = Field(default_factory=lambda: [])
File without changes
@@ -0,0 +1,77 @@
1
+ import uuid
2
+ from enum import Enum
3
+
4
+ from ophyd_async.core import StrictEnum
5
+ from pydantic import BaseModel, Field
6
+
7
+ from dodal.devices.electron_analyser.base_region import BaseRegion, BaseSequence
8
+
9
+
10
+ class Status(str, Enum):
11
+ READY = "Ready"
12
+ RUNNING = "Running"
13
+ COMPLETED = "Completed"
14
+ INVALID = "Invalid"
15
+ ABORTED = "Aborted"
16
+
17
+
18
+ class DetectorMode(StrictEnum):
19
+ ADC = "ADC"
20
+ PULSE_COUNTING = "Pulse Counting"
21
+
22
+
23
+ class AcquisitionMode(str, Enum):
24
+ SWEPT = "Swept"
25
+ FIXED = "Fixed"
26
+
27
+
28
+ class VGScientaRegion(BaseRegion):
29
+ # Override defaults of base region class
30
+ lensMode: str = "Angular45"
31
+ passEnergy: int | float = 5
32
+ acquisitionMode: str = AcquisitionMode.SWEPT
33
+ lowEnergy: float = 8.0
34
+ highEnergy: float = 10.0
35
+ stepTime: float = 1.0
36
+ energyStep: float = Field(default=200.0)
37
+ # Specific to this class
38
+ regionId: str = Field(default=str(uuid.uuid4()))
39
+ excitationEnergySource: str = "source1"
40
+ fixEnergy: float = 9.0
41
+ totalSteps: float = 13.0
42
+ totalTime: float = 13.0
43
+ exposureTime: float = 1.0
44
+ firstXChannel: int = 1
45
+ lastXChannel: int = 1000
46
+ firstYChannel: int = 101
47
+ lastYChannel: int = 800
48
+ detectorMode: DetectorMode = DetectorMode.ADC
49
+ status: Status = Status.READY
50
+
51
+
52
+ class VGScientaExcitationEnergySource(BaseModel):
53
+ name: str = "source1"
54
+ scannableName: str = ""
55
+ value: float = 0
56
+
57
+
58
+ class VGScientaSequence(BaseSequence):
59
+ elementSet: str = Field(default="Unknown")
60
+ excitationEnergySources: list[VGScientaExcitationEnergySource] = Field(
61
+ default_factory=lambda: []
62
+ )
63
+ regions: list[VGScientaRegion] = Field(default_factory=lambda: [])
64
+
65
+ def get_excitation_energy_source_by_region(
66
+ self, region: VGScientaRegion
67
+ ) -> VGScientaExcitationEnergySource | None:
68
+ filtered_excitation_energy_sources = [
69
+ e
70
+ for e in self.excitationEnergySources
71
+ if e.name == region.excitationEnergySource
72
+ ]
73
+ return (
74
+ filtered_excitation_energy_sources[0]
75
+ if len(filtered_excitation_energy_sources) == 1
76
+ else None
77
+ )
@@ -23,7 +23,7 @@ from ophyd_async.epics.core import (
23
23
  from pydantic import field_validator
24
24
  from pydantic.dataclasses import dataclass
25
25
 
26
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
26
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
27
27
  from dodal.log import LOGGER
28
28
  from dodal.parameters.experiment_parameter_base import AbstractExperimentWithBeamParams
29
29
 
@@ -203,7 +203,7 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
203
203
  self.stop_cmd = epics_signal_x(f"{prefix}STOP.PROC")
204
204
  self.status = epics_signal_r(int, f"{prefix}SCAN_STATUS")
205
205
 
206
- self.expected_images = create_hardware_backed_soft_signal(
206
+ self.expected_images = create_r_hardware_backed_soft_signal(
207
207
  float, self._calculate_expected_images
208
208
  )
209
209
 
@@ -5,7 +5,7 @@ from ophyd_async.core import StandardReadable, StrictEnum
5
5
  from ophyd_async.epics.motor import Motor
6
6
 
7
7
  from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
8
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
8
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
9
9
 
10
10
 
11
11
  class BeamstopPositions(StrictEnum):
@@ -53,7 +53,7 @@ class Beamstop(StandardReadable):
53
53
  self.x_mm = Motor(prefix + "X")
54
54
  self.y_mm = Motor(prefix + "Y")
55
55
  self.z_mm = Motor(prefix + "Z")
56
- self.selected_pos = create_hardware_backed_soft_signal(
56
+ self.selected_pos = create_r_hardware_backed_soft_signal(
57
57
  BeamstopPositions, self._get_selected_position
58
58
  )
59
59
 
@@ -3,7 +3,6 @@ from ophyd_async.epics import adcore
3
3
 
4
4
  from dodal.common.beamlines.device_helpers import CAM_SUFFIX, HDF5_SUFFIX
5
5
  from dodal.devices.i13_1.merlin_controller import MerlinController
6
- from dodal.devices.i13_1.merlin_io import MerlinDriverIO
7
6
 
8
7
 
9
8
  class Merlin(StandardDetector):
@@ -18,7 +17,7 @@ class Merlin(StandardDetector):
18
17
  fileio_suffix=HDF5_SUFFIX,
19
18
  name: str = "",
20
19
  ):
21
- self.drv = MerlinDriverIO(prefix + drv_suffix)
20
+ self.drv = adcore.ADBaseIO(prefix + drv_suffix)
22
21
  self.hdf = adcore.NDFileHDFIO(prefix + fileio_suffix)
23
22
 
24
23
  super().__init__(
@@ -6,17 +6,21 @@ from ophyd_async.core import (
6
6
  AsyncStatus,
7
7
  TriggerInfo,
8
8
  )
9
- from ophyd_async.epics import adcore
10
- from ophyd_async.epics.adcore import ADBaseController
11
-
12
- from dodal.devices.i13_1.merlin_io import MerlinDriverIO, MerlinImageMode
9
+ from ophyd_async.epics.adcore import (
10
+ DEFAULT_GOOD_STATES,
11
+ ADBaseController,
12
+ ADBaseIO,
13
+ ADImageMode,
14
+ ADState,
15
+ stop_busy_record,
16
+ )
13
17
 
14
18
 
15
19
  class MerlinController(ADBaseController):
16
20
  def __init__(
17
21
  self,
18
- driver: MerlinDriverIO,
19
- good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
22
+ driver: ADBaseIO,
23
+ good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
20
24
  ) -> None:
21
25
  self.driver = driver
22
26
  self.good_states = good_states
@@ -34,7 +38,7 @@ class MerlinController(ADBaseController):
34
38
  )
35
39
  await asyncio.gather(
36
40
  self.driver.num_images.set(trigger_info.total_number_of_triggers),
37
- self.driver.image_mode.set(MerlinImageMode.MULTIPLE),
41
+ self.driver.image_mode.set(ADImageMode.MULTIPLE),
38
42
  )
39
43
 
40
44
  async def wait_for_idle(self):
@@ -44,4 +48,4 @@ class MerlinController(ADBaseController):
44
48
  async def disarm(self):
45
49
  # We can't use caput callback as we already used it in arm() and we can't have
46
50
  # 2 or they will deadlock
47
- await adcore.stop_busy_record(self.driver.acquire, False, timeout=1)
51
+ await stop_busy_record(self.driver.acquire, False, timeout=1)
@@ -0,0 +1,30 @@
1
+ from ophyd_async.core import StandardReadable, StrictEnum
2
+ from ophyd_async.epics.core import epics_signal_rw, epics_signal_x
3
+ from ophyd_async.epics.motor import Motor
4
+
5
+
6
+ class HomeGroup(StrictEnum):
7
+ NONE = "none"
8
+ ALL = "All"
9
+ X = "X"
10
+ Y = "Y"
11
+ Z = "Z"
12
+
13
+
14
+ class HomingControl(StandardReadable):
15
+ def __init__(self, prefix: str, name: str = "") -> None:
16
+ self.homing_group = epics_signal_rw(HomeGroup, f"{prefix}:HMGRP")
17
+ self.home = epics_signal_x(f"{prefix}:HOME")
18
+ super().__init__(name)
19
+
20
+
21
+ class BeamStop(StandardReadable):
22
+ def __init__(self, prefix: str, name: str = "") -> None:
23
+ with self.add_children_as_readables():
24
+ self.x = Motor(f"{prefix}X")
25
+ self.y = Motor(f"{prefix}Y")
26
+ self.z = Motor(f"{prefix}Z")
27
+
28
+ self.homing = HomingControl(f"{prefix}HM", name)
29
+
30
+ super().__init__(name)
@@ -1,6 +1,8 @@
1
1
  from ophyd_async.core import StandardReadable
2
2
  from ophyd_async.epics.core import epics_signal_r
3
3
 
4
+ ACCESS_DEVICE_NAME = "access_control" # Device name in i19-blueapi
5
+
4
6
 
5
7
  class HutchAccessControl(StandardReadable):
6
8
  def __init__(self, prefix: str, name: str = "") -> None:
@@ -1,57 +1,79 @@
1
1
  from enum import Enum
2
2
 
3
+ from aiohttp import ClientSession
3
4
  from bluesky.protocols import Movable
4
- from ophyd_async.core import AsyncStatus, StandardReadable
5
+ from ophyd_async.core import AsyncStatus, StandardReadable, StandardReadableFormat
5
6
  from ophyd_async.epics.core import epics_signal_r
6
7
 
7
- from dodal.devices.hutch_shutter import HutchShutter, ShutterDemand
8
+ from dodal.devices.hutch_shutter import ShutterDemand, ShutterState
9
+ from dodal.devices.i19.hutch_access import ACCESS_DEVICE_NAME
8
10
  from dodal.log import LOGGER
9
11
 
10
-
11
- class HutchInvalidError(Exception):
12
- pass
12
+ OPTICS_BLUEAPI_URL = "https://i19-blueapi.diamond.ac.uk"
13
13
 
14
14
 
15
15
  class HutchState(str, Enum):
16
16
  EH1 = "EH1"
17
17
  EH2 = "EH2"
18
- INVALID = "INVALID"
19
18
 
20
19
 
21
- class HutchConditionalShutter(StandardReadable, Movable[ShutterDemand]):
20
+ class AccessControlledShutter(StandardReadable, Movable[ShutterDemand]):
22
21
  """ I19-specific device to operate the hutch shutter.
23
22
 
24
- This device evaluates the hutch state value to work out which of the two I19 \
25
- hutches is in use and then implements the HutchShutter device to operate the \
26
- experimental shutter.
23
+ This device will send a REST call to the blueapi instance controlling the optics \
24
+ hutch running on the I19 cluster, which will evaluate the current hutch in use vs \
25
+ the hutch sending the request and decide if the plan will be run or not.
27
26
  As the two hutches are located in series, checking the hutch in use is necessary to \
28
27
  avoid accidentally operating the shutter from one hutch while the other has beamtime.
29
28
 
30
- The hutch name should be passed to the device upon instantiation. If this does not \
31
- coincide with the current hutch in use, a warning will be logged and the shutter \
32
- will not be operated. This is to allow for testing of plans.
33
- An error will instead be raised if the hutch state reads as "INVALID".
29
+ The name of the hutch that wants to operate the shutter should be passed to the \
30
+ device upon instantiation.
31
+
32
+ For details see the architecture described in \
33
+ https://github.com/DiamondLightSource/i19-bluesky/issues/30.
34
34
  """
35
35
 
36
36
  def __init__(self, prefix: str, hutch: HutchState, name: str = "") -> None:
37
- self.shutter = HutchShutter(prefix=prefix, name=name)
38
- bl_prefix = prefix.split("-")[0]
39
- self.hutch_state = epics_signal_r(str, f"{bl_prefix}-OP-STAT-01:EHStatus.VALA")
37
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
38
+ self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
40
39
  self.hutch_request = hutch
40
+ self.url = OPTICS_BLUEAPI_URL
41
41
  super().__init__(name)
42
42
 
43
43
  @AsyncStatus.wrap
44
44
  async def set(self, value: ShutterDemand):
45
- hutch_in_use = await self.hutch_state.get_value()
46
- LOGGER.info(f"Current hutch in use: {hutch_in_use}")
47
- if hutch_in_use == HutchState.INVALID:
48
- raise HutchInvalidError(
49
- "The hutch state is invalid. Contact the beamline staff."
50
- )
51
- if hutch_in_use != self.hutch_request:
52
- # NOTE Warn but don't fail
53
- LOGGER.warning(
54
- f"{self.hutch_request} is not the hutch in use. Shutter will not be operated."
55
- )
56
- else:
57
- await self.shutter.set(value)
45
+ REQUEST_PARAMS = {
46
+ "name": "operate_shutter_plan",
47
+ "params": {
48
+ "experiment_hutch": self.hutch_request.value,
49
+ "access_device": ACCESS_DEVICE_NAME,
50
+ "shutter_demand": value,
51
+ },
52
+ }
53
+ async with ClientSession(base_url=self.url, raise_for_status=True) as session:
54
+ # First submit the plan to the worker
55
+ async with session.post("/tasks", data=REQUEST_PARAMS) as response:
56
+ LOGGER.debug(
57
+ f"Task submitted to the worker, response status: {response.status}"
58
+ )
59
+
60
+ try:
61
+ data = await response.json()
62
+ task_id = data["task_id"]
63
+ except Exception as e:
64
+ LOGGER.error(
65
+ f"Failed to get task_id from {self.url}/tasks POST. ({e})"
66
+ )
67
+ raise
68
+ # Then set the task as active and run asap
69
+ async with session.put(
70
+ "/worker/tasks", data={"task_id": task_id}
71
+ ) as response:
72
+ if not response.ok:
73
+ LOGGER.error(
74
+ f"""Unable to operate the shutter.
75
+ Session PUT responded with {response.status}: {response.reason}.
76
+ """
77
+ )
78
+ return
79
+ LOGGER.debug(f"Run operate shutter plan, task_id: {task_id}")
@@ -6,7 +6,7 @@ from typing import TypeVar
6
6
  from bluesky.protocols import Reading
7
7
  from event_model.documents.event_descriptor import DataKey
8
8
  from ophyd_async.core import PathProvider
9
- from ophyd_async.epics.adaravis import AravisController, AravisDetector
9
+ from ophyd_async.epics.adaravis import AravisDetector
10
10
  from ophyd_async.epics.adpilatus import PilatusDetector
11
11
 
12
12
  ValueAndUnits = tuple[float, str]
@@ -149,7 +149,6 @@ class NXSasOAV(AravisDetector):
149
149
  fileio_suffix: str,
150
150
  metadata_holder: NXSasMetadataHolder,
151
151
  name: str = "",
152
- gpio_number: AravisController.GPIO_NUMBER = 1,
153
152
  ):
154
153
  """Extends detector with configuration metadata required or desired
155
154
  to comply with the NXsas application definition.
@@ -162,7 +161,6 @@ class NXSasOAV(AravisDetector):
162
161
  drv_suffix=drv_suffix,
163
162
  fileio_suffix=fileio_suffix,
164
163
  name=name,
165
- gpio_number=gpio_number,
166
164
  )
167
165
  self._metadata_holder = metadata_holder
168
166
 
@@ -1,7 +1,7 @@
1
1
  from ophyd_async.core import StandardReadable, StrictEnum
2
2
  from ophyd_async.epics.core import epics_signal_rw
3
3
 
4
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
4
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
5
5
 
6
6
 
7
7
  class HFocusMode(StrictEnum):
@@ -40,10 +40,10 @@ class FocusMirrorsMode(StandardReadable):
40
40
  self.vertical = epics_signal_rw(VFocusMode, prefix + "G0:TARGETAPPLY")
41
41
 
42
42
  with self.add_children_as_readables():
43
- self.beam_size_x = create_hardware_backed_soft_signal(
43
+ self.beam_size_x = create_r_hardware_backed_soft_signal(
44
44
  int, self._get_beam_size_x, units="um"
45
45
  )
46
- self.beam_size_y = create_hardware_backed_soft_signal(
46
+ self.beam_size_y = create_r_hardware_backed_soft_signal(
47
47
  int, self._get_beam_size_y, units="um"
48
48
  )
49
49
 
@@ -5,7 +5,7 @@ import re
5
5
  from ophyd_async.core import StandardReadable
6
6
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
7
7
 
8
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
8
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
9
9
 
10
10
 
11
11
  class PilatusMetadata(StandardReadable):
@@ -14,7 +14,7 @@ class PilatusMetadata(StandardReadable):
14
14
  self.template = epics_signal_r(str, prefix + "cam1:FileTemplate_RBV")
15
15
  self.filenumber = epics_signal_r(int, prefix + "cam1:FileNumber_RBV")
16
16
  with self.add_children_as_readables():
17
- self.filename_template = create_hardware_backed_soft_signal(
17
+ self.filename_template = create_r_hardware_backed_soft_signal(
18
18
  str, self._get_full_filename_template
19
19
  )
20
20
  super().__init__(name)
@@ -9,10 +9,10 @@ from ophyd_async.core import (
9
9
  )
10
10
  from ophyd_async.epics.core import epics_signal_rw
11
11
 
12
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
12
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
13
13
  from dodal.devices.areadetector.plugins.CAM import Cam
14
14
  from dodal.devices.oav.oav_parameters import DEFAULT_OAV_WINDOW, OAVConfig
15
- from dodal.devices.oav.snapshots.snapshot_with_beam_centre import SnapshotWithBeamCentre
15
+ from dodal.devices.oav.snapshots.snapshot import Snapshot
16
16
  from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
17
17
 
18
18
 
@@ -63,24 +63,22 @@ class OAV(StandardReadable):
63
63
 
64
64
  with self.add_children_as_readables():
65
65
  self.grid_snapshot = SnapshotWithGrid(f"{prefix}MJPG:", name)
66
- self.microns_per_pixel_x = create_hardware_backed_soft_signal(
66
+ self.microns_per_pixel_x = create_r_hardware_backed_soft_signal(
67
67
  float,
68
68
  lambda: self._get_microns_per_pixel(Coords.X),
69
69
  )
70
- self.microns_per_pixel_y = create_hardware_backed_soft_signal(
70
+ self.microns_per_pixel_y = create_r_hardware_backed_soft_signal(
71
71
  float,
72
72
  lambda: self._get_microns_per_pixel(Coords.Y),
73
73
  )
74
- self.beam_centre_i = create_hardware_backed_soft_signal(
74
+ self.beam_centre_i = create_r_hardware_backed_soft_signal(
75
75
  int, lambda: self._get_beam_position(Coords.X)
76
76
  )
77
- self.beam_centre_j = create_hardware_backed_soft_signal(
77
+ self.beam_centre_j = create_r_hardware_backed_soft_signal(
78
78
  int, lambda: self._get_beam_position(Coords.Y)
79
79
  )
80
- self.snapshot = SnapshotWithBeamCentre(
80
+ self.snapshot = Snapshot(
81
81
  f"{self._prefix}MJPG:",
82
- self.beam_centre_i,
83
- self.beam_centre_j,
84
82
  self._name,
85
83
  )
86
84