dls-dodal 1.29.3__py3-none-any.whl → 1.30.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 (88) hide show
  1. {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/METADATA +28 -43
  2. dls_dodal-1.30.0.dist-info/RECORD +132 -0
  3. {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/WHEEL +1 -1
  4. dls_dodal-1.30.0.dist-info/entry_points.txt +3 -0
  5. dodal/__init__.py +1 -4
  6. dodal/_version.py +2 -2
  7. dodal/beamlines/__init__.py +3 -1
  8. dodal/beamlines/i03.py +28 -23
  9. dodal/beamlines/i04.py +34 -12
  10. dodal/beamlines/i13_1.py +66 -0
  11. dodal/beamlines/i22.py +5 -5
  12. dodal/beamlines/i24.py +15 -1
  13. dodal/beamlines/p38.py +7 -7
  14. dodal/beamlines/p45.py +7 -5
  15. dodal/beamlines/p99.py +61 -0
  16. dodal/cli.py +6 -3
  17. dodal/common/beamlines/beamline_parameters.py +2 -2
  18. dodal/common/beamlines/beamline_utils.py +6 -5
  19. dodal/common/maths.py +1 -3
  20. dodal/common/types.py +2 -3
  21. dodal/common/udc_directory_provider.py +14 -3
  22. dodal/common/visit.py +2 -3
  23. dodal/devices/CTAB.py +22 -17
  24. dodal/devices/aperturescatterguard.py +114 -136
  25. dodal/devices/areadetector/adaravis.py +8 -6
  26. dodal/devices/areadetector/adsim.py +2 -3
  27. dodal/devices/areadetector/adutils.py +20 -12
  28. dodal/devices/areadetector/plugins/MJPG.py +0 -4
  29. dodal/devices/attenuator.py +4 -4
  30. dodal/devices/cryostream.py +19 -7
  31. dodal/devices/detector/__init__.py +13 -2
  32. dodal/devices/detector/det_dim_constants.py +2 -2
  33. dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
  34. dodal/devices/detector/detector.py +8 -7
  35. dodal/devices/detector/detector_motion.py +38 -31
  36. dodal/devices/eiger.py +23 -23
  37. dodal/devices/eiger_odin.py +12 -13
  38. dodal/devices/fast_grid_scan.py +4 -3
  39. dodal/devices/fluorescence_detector_motion.py +13 -4
  40. dodal/devices/focusing_mirror.py +66 -66
  41. dodal/devices/hutch_shutter.py +4 -4
  42. dodal/devices/i22/dcm.py +4 -3
  43. dodal/devices/i22/fswitch.py +4 -4
  44. dodal/devices/i22/nxsas.py +23 -32
  45. dodal/devices/i24/dcm.py +42 -0
  46. dodal/devices/i24/pmac.py +47 -8
  47. dodal/devices/ipin.py +7 -4
  48. dodal/devices/linkam3.py +11 -5
  49. dodal/devices/logging_ophyd_device.py +1 -1
  50. dodal/devices/motors.py +31 -5
  51. dodal/devices/oav/grid_overlay.py +1 -0
  52. dodal/devices/oav/microns_for_zoom_levels.json +1 -1
  53. dodal/devices/oav/oav_detector.py +2 -0
  54. dodal/devices/oav/oav_parameters.py +18 -10
  55. dodal/devices/oav/oav_to_redis_forwarder.py +100 -0
  56. dodal/devices/oav/pin_image_recognition/__init__.py +19 -17
  57. dodal/devices/oav/pin_image_recognition/utils.py +5 -6
  58. dodal/devices/oav/utils.py +3 -17
  59. dodal/devices/p99/__init__.py +0 -0
  60. dodal/devices/p99/sample_stage.py +43 -0
  61. dodal/devices/robot.py +30 -18
  62. dodal/devices/scintillator.py +8 -5
  63. dodal/devices/smargon.py +3 -3
  64. dodal/devices/status.py +2 -31
  65. dodal/devices/tetramm.py +4 -4
  66. dodal/devices/thawer.py +5 -3
  67. dodal/devices/undulator_dcm.py +6 -8
  68. dodal/devices/util/adjuster_plans.py +2 -2
  69. dodal/devices/util/epics_util.py +6 -8
  70. dodal/devices/util/lookup_tables.py +2 -3
  71. dodal/devices/util/save_panda.py +87 -0
  72. dodal/devices/util/test_utils.py +17 -0
  73. dodal/devices/webcam.py +3 -8
  74. dodal/devices/xbpm_feedback.py +0 -23
  75. dodal/devices/zebra.py +10 -10
  76. dodal/devices/zebra_controlled_shutter.py +3 -3
  77. dodal/devices/zocalo/zocalo_interaction.py +10 -2
  78. dodal/devices/zocalo/zocalo_results.py +31 -18
  79. dodal/log.py +14 -5
  80. dodal/plans/data_session_metadata.py +1 -0
  81. dodal/plans/motor_util_plans.py +117 -0
  82. dodal/utils.py +74 -26
  83. dls_dodal-1.29.3.dist-info/RECORD +0 -124
  84. dls_dodal-1.29.3.dist-info/entry_points.txt +0 -2
  85. dodal/devices/qbpm1.py +0 -8
  86. {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/LICENSE +0 -0
  87. {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/top_level.txt +0 -0
  88. /dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +0 -0
@@ -1,19 +1,16 @@
1
+ from collections.abc import Generator
1
2
  from enum import IntEnum
2
- from pathlib import Path
3
- from typing import Generator, Tuple
4
3
 
5
4
  import bluesky.plan_stubs as bps
6
5
  import numpy as np
7
6
  from bluesky.utils import Msg
8
- from PIL.Image import Image
9
7
 
10
8
  from dodal.devices.oav.oav_calculations import camera_coordinates_to_xyz
11
- from dodal.devices.oav.oav_parameters import OAVConfigParams
9
+ from dodal.devices.oav.oav_detector import OAVConfigParams
12
10
  from dodal.devices.oav.pin_image_recognition import PinTipDetection
13
11
  from dodal.devices.smargon import Smargon
14
- from dodal.log import LOGGER
15
12
 
16
- Pixel = Tuple[int, int]
13
+ Pixel = tuple[int, int]
17
14
 
18
15
 
19
16
  class PinNotFoundException(Exception):
@@ -110,14 +107,3 @@ def wait_for_tip_to_be_found(
110
107
  raise PinNotFoundException(f"No pin found after {timeout} seconds")
111
108
 
112
109
  return found_tip # type: ignore
113
-
114
-
115
- def save_thumbnail(full_file_path: Path, full_image: Image, new_height=192):
116
- """Scales an image down to have the height specified in new_height and saves it
117
- to the same location as the full image with a t appended to the filename"""
118
- thumbnail_path = full_file_path.with_stem(full_file_path.stem + "t")
119
- LOGGER.info(f"Saving thumbnail to {thumbnail_path}")
120
- full_size = full_image.size
121
- new_width = (new_height / full_size[1]) * full_size[0]
122
- full_image.thumbnail((new_width, new_height))
123
- full_image.save(thumbnail_path.as_posix())
File without changes
@@ -0,0 +1,43 @@
1
+ from enum import Enum
2
+
3
+ from ophyd_async.core import Device
4
+ from ophyd_async.epics.signal import epics_signal_rw
5
+
6
+
7
+ class SampleAngleStage(Device):
8
+ def __init__(self, prefix: str, name: str):
9
+ self.theta = epics_signal_rw(
10
+ float, prefix + "WRITETHETA:RBV", prefix + "WRITETHETA"
11
+ )
12
+ self.roll = epics_signal_rw(
13
+ float, prefix + "WRITEROLL:RBV", prefix + "WRITEROLL"
14
+ )
15
+ self.pitch = epics_signal_rw(
16
+ float, prefix + "WRITEPITCH:RBV", prefix + "WRITEPITCH"
17
+ )
18
+ super().__init__(name=name)
19
+
20
+
21
+ class p99StageSelections(str, Enum):
22
+ Empty = "Empty"
23
+ Mn5um = "Mn 5um"
24
+ Fe = "Fe (empty)"
25
+ Co5um = "Co 5um"
26
+ Ni5um = "Ni 5um"
27
+ Cu5um = "Cu 5um"
28
+ Zn5um = "Zn 5um"
29
+ Zr = "Zr (empty)"
30
+ Mo = "Mo (empty)"
31
+ Rh = "Rh (empty)"
32
+ Pd = "Pd (empty)"
33
+ Ag = "Ag (empty)"
34
+ Cd25um = "Cd 25um"
35
+ W = "W (empty)"
36
+ Pt = "Pt (empty)"
37
+ User = "User"
38
+
39
+
40
+ class FilterMotor(Device):
41
+ def __init__(self, prefix: str, name: str):
42
+ self.user_setpoint = epics_signal_rw(p99StageSelections, prefix)
43
+ super().__init__(name=name)
dodal/devices/robot.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from asyncio import FIRST_COMPLETED, Task
2
+ from asyncio import FIRST_COMPLETED, CancelledError, Task
3
3
  from dataclasses import dataclass
4
4
  from enum import Enum
5
5
 
@@ -45,6 +45,9 @@ class BartRobot(StandardReadable, Movable):
45
45
  LOAD_TIMEOUT = 60
46
46
  NO_PIN_ERROR_CODE = 25
47
47
 
48
+ # How far the gonio position can be out before loading will fail
49
+ LOAD_TOLERANCE_MM = 0.02
50
+
48
51
  def __init__(
49
52
  self,
50
53
  name: str,
@@ -74,19 +77,28 @@ class BartRobot(StandardReadable, Movable):
74
77
  await wait_for_value(self.error_code, self.NO_PIN_ERROR_CODE, None)
75
78
  raise RobotLoadFailed(self.NO_PIN_ERROR_CODE, "Pin was not detected")
76
79
 
77
- finished, unfinished = await asyncio.wait(
78
- [
79
- Task(raise_if_no_pin()),
80
- Task(
81
- wait_for_value(self.gonio_pin_sensor, PinMounted.PIN_MOUNTED, None)
82
- ),
83
- ],
84
- return_when=FIRST_COMPLETED,
85
- )
86
- for task in unfinished:
87
- task.cancel()
88
- for task in finished:
89
- await task
80
+ async def wfv():
81
+ await wait_for_value(self.gonio_pin_sensor, PinMounted.PIN_MOUNTED, None)
82
+
83
+ tasks = [
84
+ (Task(raise_if_no_pin())),
85
+ (Task(wfv())),
86
+ ]
87
+ try:
88
+ finished, unfinished = await asyncio.wait(
89
+ tasks,
90
+ return_when=FIRST_COMPLETED,
91
+ )
92
+ for task in unfinished:
93
+ task.cancel()
94
+ for task in finished:
95
+ await task
96
+ except CancelledError:
97
+ # If the outer enclosing task cancels after LOAD_TIMEOUT, this causes CancelledError to be raised
98
+ # in the current task, when it propagates to here we should cancel all pending tasks before bubbling up
99
+ for task in tasks:
100
+ task.cancel()
101
+ raise
90
102
 
91
103
  async def _load_pin_and_puck(self, sample_location: SampleLocation):
92
104
  LOGGER.info(f"Loading pin {sample_location}")
@@ -108,12 +120,12 @@ class BartRobot(StandardReadable, Movable):
108
120
  await self.pin_mounted_or_no_pin_found()
109
121
 
110
122
  @AsyncStatus.wrap
111
- async def set(self, sample_location: SampleLocation):
123
+ async def set(self, value: SampleLocation):
112
124
  try:
113
125
  await asyncio.wait_for(
114
- self._load_pin_and_puck(sample_location), timeout=self.LOAD_TIMEOUT
126
+ self._load_pin_and_puck(value), timeout=self.LOAD_TIMEOUT
115
127
  )
116
- except asyncio.TimeoutError:
128
+ except asyncio.TimeoutError as e:
117
129
  error_code = await self.error_code.get_value()
118
130
  error_string = await self.error_str.get_value()
119
- raise RobotLoadFailed(error_code, error_string)
131
+ raise RobotLoadFailed(int(error_code), error_string) from e
@@ -1,7 +1,10 @@
1
- from ophyd import Component as Cpt
2
- from ophyd import Device, EpicsMotor
1
+ from ophyd_async.core import StandardReadable
2
+ from ophyd_async.epics.motion import Motor
3
3
 
4
4
 
5
- class Scintillator(Device):
6
- y = Cpt(EpicsMotor, "-MO-SCIN-01:Y")
7
- z = Cpt(EpicsMotor, "-MO-SCIN-01:Z")
5
+ class Scintillator(StandardReadable):
6
+ def __init__(self, prefix: str, name: str = ""):
7
+ with self.add_children_as_readables():
8
+ self.y = Motor(prefix + "-MO-SCIN-01:Y")
9
+ self.z = Motor(prefix + "-MO-SCIN-01:Z")
10
+ super().__init__(name)
dodal/devices/smargon.py CHANGED
@@ -1,8 +1,8 @@
1
- from collections.abc import Generator
1
+ from collections.abc import Collection, Generator
2
2
  from dataclasses import dataclass
3
3
  from enum import Enum
4
4
  from math import isclose
5
- from typing import Collection, cast
5
+ from typing import cast
6
6
 
7
7
  from bluesky import plan_stubs as bps
8
8
  from bluesky.utils import Msg
@@ -87,7 +87,7 @@ class XYZLimits:
87
87
  def position_valid(self, pos: Collection[float]) -> bool:
88
88
  return all(
89
89
  axis_limits.contains(value)
90
- for axis_limits, value in zip([self.x, self.y, self.z], pos)
90
+ for axis_limits, value in zip([self.x, self.y, self.z], pos, strict=False)
91
91
  )
92
92
 
93
93
 
dodal/devices/status.py CHANGED
@@ -1,41 +1,12 @@
1
- from typing import Any, TypeVar
1
+ from typing import Any
2
2
 
3
3
  from ophyd.status import SubscriptionStatus
4
4
 
5
- T = TypeVar("T")
6
-
7
5
 
8
6
  def await_value(
9
- subscribable: Any, expected_value: T, timeout: None | int = None
7
+ subscribable: Any, expected_value: object, timeout: None | int = None
10
8
  ) -> SubscriptionStatus:
11
9
  def value_is(value, **_):
12
10
  return value == expected_value
13
11
 
14
12
  return SubscriptionStatus(subscribable, value_is, timeout=timeout)
15
-
16
-
17
- def await_value_in_list(
18
- subscribable: Any, expected_value: list, timeout: None | int = None
19
- ) -> SubscriptionStatus:
20
- """Returns a status which is completed when the subscriptable contains a value
21
- within the expected_value list"""
22
-
23
- def value_is(value, **_):
24
- return value in expected_value
25
-
26
- if not isinstance(expected_value, list):
27
- raise TypeError(f"expected value {expected_value} is not a list")
28
- else:
29
- return SubscriptionStatus(subscribable, value_is, timeout=timeout)
30
-
31
-
32
- def await_approx_value(
33
- subscribable: Any,
34
- expected_value: T,
35
- deadband: float = 1e-09,
36
- timeout: None | int = None,
37
- ) -> SubscriptionStatus:
38
- def value_is_approx(value, **_):
39
- return abs(value - expected_value) <= deadband
40
-
41
- return SubscriptionStatus(subscribable, value_is_approx, timeout=timeout)
dodal/devices/tetramm.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
+ from collections.abc import Sequence
2
3
  from enum import Enum
3
- from typing import Sequence
4
4
 
5
5
  from bluesky.protocols import Hints
6
6
  from ophyd_async.core import (
@@ -115,7 +115,7 @@ class TetrammController(DetectorControl):
115
115
  async def arm(
116
116
  self,
117
117
  num: int,
118
- trigger: DetectorTrigger,
118
+ trigger: DetectorTrigger = DetectorTrigger.edge_trigger,
119
119
  exposure: float | None = None,
120
120
  ) -> AsyncStatus:
121
121
  if exposure is None:
@@ -132,7 +132,7 @@ class TetrammController(DetectorControl):
132
132
  self._drv.averaging_time.set(exposure), self.set_exposure(exposure)
133
133
  )
134
134
 
135
- status = await set_and_wait_for_value(self._drv.acquire, 1)
135
+ status = await set_and_wait_for_value(self._drv.acquire, True)
136
136
 
137
137
  return status
138
138
 
@@ -150,7 +150,7 @@ class TetrammController(DetectorControl):
150
150
  )
151
151
 
152
152
  async def disarm(self):
153
- await stop_busy_record(self._drv.acquire, 0, timeout=1)
153
+ await stop_busy_record(self._drv.acquire, False, timeout=1)
154
154
 
155
155
  async def set_exposure(self, exposure: float):
156
156
  """Tries to set the exposure time of a single frame.
dodal/devices/thawer.py CHANGED
@@ -15,7 +15,7 @@ class ThawerStates(str, Enum):
15
15
  ON = "On"
16
16
 
17
17
 
18
- class ThawingTimer(Device):
18
+ class ThawingTimer(Device, Stoppable):
19
19
  def __init__(self, control_signal: SignalRW[ThawerStates]) -> None:
20
20
  self._control_signal = control_signal
21
21
  self._thawing_task: Task | None = None
@@ -32,7 +32,8 @@ class ThawingTimer(Device):
32
32
  finally:
33
33
  await self._control_signal.set(ThawerStates.OFF)
34
34
 
35
- async def stop(self):
35
+ @AsyncStatus.wrap
36
+ async def stop(self, *args, **kwargs):
36
37
  if self._thawing_task:
37
38
  self._thawing_task.cancel()
38
39
 
@@ -43,6 +44,7 @@ class Thawer(StandardReadable, Stoppable):
43
44
  self.thaw_for_time_s = ThawingTimer(self.control)
44
45
  super().__init__(name)
45
46
 
46
- async def stop(self):
47
+ @AsyncStatus.wrap
48
+ async def stop(self, *args, **kwargs):
47
49
  await self.thaw_for_time_s.stop()
48
50
  await self.control.set(ThawerStates.OFF)
@@ -74,14 +74,12 @@ class UndulatorDCM(StandardReadable, Movable):
74
74
  daq_configuration_path + "/domain/beamlineParameters"
75
75
  )["DCM_Perp_Offset_FIXED"]
76
76
 
77
- def set(self, value: float) -> AsyncStatus:
78
- async def _set():
79
- await asyncio.gather(
80
- self._set_dcm_energy(value),
81
- self._set_undulator_gap_if_required(value),
82
- )
83
-
84
- return AsyncStatus(_set())
77
+ @AsyncStatus.wrap
78
+ async def set(self, value: float):
79
+ await asyncio.gather(
80
+ self._set_dcm_energy(value),
81
+ self._set_undulator_gap_if_required(value),
82
+ )
85
83
 
86
84
  async def _set_dcm_energy(self, energy_kev: float) -> None:
87
85
  access_level = await self.undulator.gap_access.get_value()
@@ -3,10 +3,10 @@ All the methods in this module return a bluesky plan generator that adjusts a va
3
3
  according to some criteria either via feedback, preset positions, lookup tables etc.
4
4
  """
5
5
 
6
- from typing import Callable, Generator
6
+ from collections.abc import Callable, Generator
7
7
 
8
8
  from bluesky import plan_stubs as bps
9
- from bluesky.run_engine import Msg
9
+ from bluesky.utils import Msg
10
10
  from ophyd.epics_motor import EpicsMotor
11
11
  from ophyd_async.epics.motion import Motor
12
12
 
@@ -1,5 +1,5 @@
1
+ from collections.abc import Callable, Sequence
1
2
  from functools import partial
2
- from typing import Callable
3
3
 
4
4
  from bluesky.protocols import Movable
5
5
  from ophyd import Component, EpicsSignal
@@ -26,7 +26,7 @@ def epics_signal_put_wait(pv_name: str, wait: float = 3.0) -> Component[EpicsSig
26
26
 
27
27
 
28
28
  def run_functions_without_blocking(
29
- functions_to_chain: list[Callable[[], StatusBase]],
29
+ functions_to_chain: Sequence[Callable[[], StatusBase]],
30
30
  timeout: float = 60.0,
31
31
  associated_obj: OphydDevice | None = None,
32
32
  ) -> Status:
@@ -61,9 +61,9 @@ def run_functions_without_blocking(
61
61
  # Wrap each function by first checking the previous status and attaching a callback
62
62
  # to the next function in the chain
63
63
  def wrap_func(
64
- old_status: Status, current_func: Callable[[], StatusBase], next_func
64
+ old_status: Status | None, current_func: Callable[[], StatusBase], next_func
65
65
  ):
66
- if old_status.exception() is not None:
66
+ if old_status is not None and old_status.exception() is not None:
67
67
  set_global_exception_and_log(old_status)
68
68
  return
69
69
 
@@ -96,7 +96,7 @@ def run_functions_without_blocking(
96
96
  )
97
97
 
98
98
  # Wrap each function in reverse
99
- for num, func in enumerate(list(reversed(functions_to_chain))[1:-1]):
99
+ for func in list(reversed(functions_to_chain))[1:-1]:
100
100
  wrapped_funcs.append(
101
101
  partial(
102
102
  wrap_func,
@@ -105,10 +105,8 @@ def run_functions_without_blocking(
105
105
  )
106
106
  )
107
107
 
108
- starting_status = Status(done=True, success=True)
109
-
110
108
  # Initiate the chain of functions
111
- wrap_func(starting_status, functions_to_chain[0], wrapped_funcs[-1])
109
+ wrap_func(None, functions_to_chain[0], wrapped_funcs[-1])
112
110
  return full_status
113
111
 
114
112
 
@@ -3,9 +3,8 @@ All the public methods in this module return a lookup table of some kind that
3
3
  converts the source value s to a target value t for different values of s.
4
4
  """
5
5
 
6
- from collections.abc import Sequence
6
+ from collections.abc import Callable, Sequence
7
7
  from io import StringIO
8
- from typing import Callable
9
8
 
10
9
  import aiofiles
11
10
  import numpy as np
@@ -36,7 +35,7 @@ async def energy_distance_table(lookup_table_path: str) -> np.ndarray:
36
35
  def linear_interpolation_lut(filename: str) -> Callable[[float], float]:
37
36
  """Returns a callable that converts values by linear interpolation of lookup table values"""
38
37
  LOGGER.info(f"Using lookup table {filename}")
39
- s_and_t_vals = zip(*loadtxt(filename, comments=["#", "Units"]))
38
+ s_and_t_vals = zip(*loadtxt(filename, comments=["#", "Units"]), strict=False)
40
39
 
41
40
  s_values: Sequence
42
41
  t_values: Sequence
@@ -0,0 +1,87 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+ from argparse import ArgumentParser
5
+ from pathlib import Path
6
+ from typing import cast
7
+
8
+ from bluesky.run_engine import RunEngine
9
+ from ophyd_async.core import Device, save_device
10
+ from ophyd_async.panda import phase_sorter
11
+
12
+ from dodal.beamlines import module_name_for_beamline
13
+ from dodal.utils import make_device
14
+
15
+
16
+ def main(argv: list[str]):
17
+ """CLI Utility to save the panda configuration."""
18
+ parser = ArgumentParser(description="Save an ophyd_async panda to yaml")
19
+ parser.add_argument(
20
+ "--beamline", help="beamline to save from e.g. i03. Defaults to BEAMLINE"
21
+ )
22
+ parser.add_argument(
23
+ "--device-name",
24
+ help='name of the device. The default is "panda"',
25
+ default="panda",
26
+ )
27
+ parser.add_argument(
28
+ "-f",
29
+ "--force",
30
+ action=argparse.BooleanOptionalAction,
31
+ help="Force overwriting an existing file",
32
+ )
33
+ parser.add_argument("output_file", help="output filename")
34
+
35
+ # this exit()s with message/help unless args parsed successfully
36
+ args = parser.parse_args(argv[1:])
37
+
38
+ beamline = args.beamline
39
+ device_name = args.device_name
40
+ output_file = args.output_file
41
+ force = args.force
42
+
43
+ if beamline:
44
+ os.environ["BEAMLINE"] = beamline
45
+ else:
46
+ beamline = os.environ.get("BEAMLINE", None)
47
+
48
+ if not beamline:
49
+ sys.stderr.write("BEAMLINE not set and --beamline not specified\n")
50
+ return 1
51
+
52
+ if Path(output_file).exists() and not force:
53
+ sys.stderr.write(
54
+ f"Output file {output_file} already exists and --force not specified."
55
+ )
56
+ return 1
57
+
58
+ _save_panda(beamline, device_name, output_file)
59
+
60
+ print("Done.")
61
+ return 0
62
+
63
+
64
+ def _save_panda(beamline, device_name, output_file):
65
+ RE = RunEngine()
66
+ print("Creating devices...")
67
+ module_name = module_name_for_beamline(beamline)
68
+ try:
69
+ devices = make_device(f"dodal.beamlines.{module_name}", device_name)
70
+ except Exception as error:
71
+ sys.stderr.write(f"Couldn't create device {device_name}: {error}\n")
72
+ sys.exit(1)
73
+
74
+ panda = devices[device_name]
75
+ print(f"Saving to {output_file} from {device_name} on {beamline}...")
76
+ _save_panda_to_file(RE, cast(Device, panda), output_file)
77
+
78
+
79
+ def _save_panda_to_file(RE: RunEngine, panda: Device, path: str):
80
+ def save_to_file():
81
+ yield from save_device(panda, path, sorter=phase_sorter)
82
+
83
+ RE(save_to_file())
84
+
85
+
86
+ if __name__ == "__main__": # pragma: no cover
87
+ sys.exit(main(sys.argv))
@@ -0,0 +1,17 @@
1
+ from ophyd_async.core import (
2
+ callback_on_mock_put,
3
+ set_mock_value,
4
+ )
5
+ from ophyd_async.epics.motion import Motor
6
+
7
+
8
+ def patch_motor(motor: Motor, initial_position=0):
9
+ set_mock_value(motor.user_setpoint, initial_position)
10
+ set_mock_value(motor.user_readback, initial_position)
11
+ set_mock_value(motor.deadband, 0.001)
12
+ set_mock_value(motor.motor_done_move, 1)
13
+ set_mock_value(motor.velocity, 3)
14
+ return callback_on_mock_put(
15
+ motor.user_setpoint,
16
+ lambda pos, *args, **kwargs: set_mock_value(motor.user_readback, pos),
17
+ )
dodal/devices/webcam.py CHANGED
@@ -1,13 +1,10 @@
1
- import io
2
1
  from pathlib import Path
3
2
 
4
3
  import aiofiles
5
4
  from aiohttp import ClientSession
6
5
  from bluesky.protocols import Triggerable
7
- from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_rw
8
- from PIL import Image
6
+ from ophyd_async.core import AsyncStatus, HintedSignal, StandardReadable, soft_signal_rw
9
7
 
10
- from dodal.devices.oav.utils import save_thumbnail
11
8
  from dodal.log import LOGGER
12
9
 
13
10
 
@@ -18,7 +15,7 @@ class Webcam(StandardReadable, Triggerable):
18
15
  self.directory = soft_signal_rw(str, name="directory")
19
16
  self.last_saved_path = soft_signal_rw(str, name="last_saved_path")
20
17
 
21
- self.set_readable_signals([self.last_saved_path])
18
+ self.add_readables([self.last_saved_path], wrapper=HintedSignal)
22
19
  super().__init__(name=name)
23
20
 
24
21
  async def _write_image(self, file_path: str):
@@ -26,10 +23,8 @@ class Webcam(StandardReadable, Triggerable):
26
23
  async with session.get(self.url) as response:
27
24
  response.raise_for_status()
28
25
  LOGGER.info(f"Saving webcam image from {self.url} to {file_path}")
29
- data = await response.read()
30
26
  async with aiofiles.open(file_path, "wb") as file:
31
- await file.write(data)
32
- save_thumbnail(Path(file_path), Image.open(io.BytesIO(data)))
27
+ await file.write(await response.read())
33
28
 
34
29
  @AsyncStatus.wrap
35
30
  async def trigger(self) -> None:
@@ -1,9 +1,6 @@
1
1
  from enum import Enum
2
2
 
3
- import ophyd
4
3
  from bluesky.protocols import Triggerable
5
- from ophyd import Component, EpicsSignal, EpicsSignalRO
6
- from ophyd.status import StatusBase, SubscriptionStatus
7
4
  from ophyd_async.core import Device, observe_value
8
5
  from ophyd_async.core.async_status import AsyncStatus
9
6
  from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
@@ -31,23 +28,3 @@ class XBPMFeedback(Device, Triggerable):
31
28
  async for value in observe_value(self.pos_stable):
32
29
  if value:
33
30
  return
34
-
35
-
36
- class XBPMFeedbackI04(ophyd.Device):
37
- """The I04 version of this device has a slightly different trigger method"""
38
-
39
- # Values to set to pause_feedback
40
- PAUSE = 0
41
- RUN = 1
42
-
43
- pos_ok = Component(EpicsSignalRO, "-EA-FDBK-01:XBPM2POSITION_OK")
44
- pause_feedback = Component(EpicsSignal, "-EA-FDBK-01:FB_PAUSE")
45
- x = Component(EpicsSignalRO, "-EA-XBPM-02:PosX:MeanValue_RBV")
46
- y = Component(EpicsSignalRO, "-EA-XBPM-02:PosY:MeanValue_RBV")
47
-
48
- def trigger(self) -> StatusBase:
49
- return SubscriptionStatus(
50
- self.pos_ok,
51
- lambda *, old_value, value, **kwargs: value == 1,
52
- timeout=60,
53
- )
dodal/devices/zebra.py CHANGED
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  from enum import Enum
5
5
  from functools import partialmethod
6
- from typing import List
7
6
 
8
7
  from ophyd_async.core import (
9
8
  AsyncStatus,
@@ -95,7 +94,7 @@ class ArmingDevice(StandardReadable):
95
94
  """A useful device that can abstract some of the logic of arming.
96
95
  Allows a user to just call arm.set(ArmDemand.ARM)"""
97
96
 
98
- TIMEOUT = 3
97
+ TIMEOUT: float = 3
99
98
 
100
99
  def __init__(self, prefix: str, name: str = "") -> None:
101
100
  self.arm_set = epics_signal_rw(float, prefix + "PC_ARM")
@@ -110,10 +109,9 @@ class ArmingDevice(StandardReadable):
110
109
  if reading == demand.value:
111
110
  return
112
111
 
113
- def set(self, demand: ArmDemand) -> AsyncStatus:
114
- return AsyncStatus(
115
- asyncio.wait_for(self._set_armed(demand), timeout=self.TIMEOUT)
116
- )
112
+ @AsyncStatus.wrap
113
+ async def set(self, demand: ArmDemand):
114
+ await asyncio.wait_for(self._set_armed(demand), timeout=self.TIMEOUT)
117
115
 
118
116
 
119
117
  class PositionCompare(StandardReadable):
@@ -166,7 +164,7 @@ class ZebraOutputPanel(StandardReadable):
166
164
  super().__init__(name)
167
165
 
168
166
 
169
- def boolean_array_to_integer(values: List[bool]) -> int:
167
+ def boolean_array_to_integer(values: list[bool]) -> int:
170
168
  """Converts a boolean array to integer by interpretting it in binary with LSB 0 bit
171
169
  numbering.
172
170
 
@@ -245,8 +243,8 @@ class LogicGateConfiguration:
245
243
  NUMBER_OF_INPUTS = 4
246
244
 
247
245
  def __init__(self, input_source: int, invert: bool = False) -> None:
248
- self.sources: List[int] = []
249
- self.invert: List[bool] = []
246
+ self.sources: list[int] = []
247
+ self.invert: list[bool] = []
250
248
  self.add_input(input_source, invert)
251
249
 
252
250
  def add_input(
@@ -271,7 +269,9 @@ class LogicGateConfiguration:
271
269
 
272
270
  def __str__(self) -> str:
273
271
  input_strings = []
274
- for input, (source, invert) in enumerate(zip(self.sources, self.invert)):
272
+ for input, (source, invert) in enumerate(
273
+ zip(self.sources, self.invert, strict=False)
274
+ ):
275
275
  input_strings.append(f"INP{input+1}={'!' if invert else ''}{source}")
276
276
 
277
277
  return ", ".join(input_strings)
@@ -29,10 +29,10 @@ class ZebraShutter(StandardReadable, Movable):
29
29
  super().__init__(name=name)
30
30
 
31
31
  @AsyncStatus.wrap
32
- async def set(self, desired_position: ZebraShutterState):
33
- await self.position_setpoint.set(desired_position)
32
+ async def set(self, value: ZebraShutterState):
33
+ await self.position_setpoint.set(value)
34
34
  return await wait_for_value(
35
35
  signal=self.position_readback,
36
- match=desired_position,
36
+ match=value,
37
37
  timeout=DEFAULT_TIMEOUT,
38
38
  )