ophyd-async 0.3a1__py3-none-any.whl → 0.3a2__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 (58) hide show
  1. ophyd_async/__init__.py +1 -4
  2. ophyd_async/_version.py +1 -1
  3. ophyd_async/core/__init__.py +12 -2
  4. ophyd_async/core/_providers.py +3 -1
  5. ophyd_async/core/detector.py +65 -38
  6. ophyd_async/core/device.py +8 -0
  7. ophyd_async/core/flyer.py +10 -19
  8. ophyd_async/core/signal.py +36 -17
  9. ophyd_async/core/signal_backend.py +5 -2
  10. ophyd_async/core/sim_signal_backend.py +28 -16
  11. ophyd_async/core/standard_readable.py +4 -2
  12. ophyd_async/core/utils.py +18 -1
  13. ophyd_async/epics/_backend/_aioca.py +13 -11
  14. ophyd_async/epics/_backend/_p4p.py +19 -16
  15. ophyd_async/epics/_backend/common.py +16 -11
  16. ophyd_async/epics/areadetector/__init__.py +4 -0
  17. ophyd_async/epics/areadetector/aravis.py +69 -0
  18. ophyd_async/epics/areadetector/controllers/aravis_controller.py +73 -0
  19. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +36 -24
  20. ophyd_async/epics/areadetector/drivers/aravis_driver.py +154 -0
  21. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +4 -4
  22. ophyd_async/epics/areadetector/pilatus.py +50 -0
  23. ophyd_async/epics/areadetector/writers/_hdffile.py +4 -4
  24. ophyd_async/epics/areadetector/writers/hdf_writer.py +6 -1
  25. ophyd_async/epics/demo/__init__.py +33 -3
  26. ophyd_async/epics/motion/motor.py +20 -14
  27. ophyd_async/epics/pvi/__init__.py +3 -0
  28. ophyd_async/epics/pvi/pvi.py +318 -0
  29. ophyd_async/epics/signal/signal.py +26 -9
  30. ophyd_async/panda/__init__.py +17 -6
  31. ophyd_async/panda/_common_blocks.py +49 -0
  32. ophyd_async/panda/_hdf_panda.py +48 -0
  33. ophyd_async/panda/{panda_controller.py → _panda_controller.py} +3 -7
  34. ophyd_async/panda/_trigger.py +39 -0
  35. ophyd_async/panda/writers/__init__.py +3 -0
  36. ophyd_async/panda/writers/_hdf_writer.py +220 -0
  37. ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
  38. ophyd_async/planstubs/__init__.py +5 -0
  39. ophyd_async/planstubs/prepare_trigger_and_dets.py +57 -0
  40. ophyd_async/protocols.py +73 -0
  41. ophyd_async/sim/__init__.py +11 -0
  42. ophyd_async/sim/demo/__init__.py +3 -0
  43. ophyd_async/sim/demo/sim_motor.py +116 -0
  44. ophyd_async/sim/pattern_generator.py +318 -0
  45. ophyd_async/sim/sim_pattern_detector_control.py +55 -0
  46. ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
  47. ophyd_async/sim/sim_pattern_generator.py +37 -0
  48. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/METADATA +19 -75
  49. ophyd_async-0.3a2.dist-info/RECORD +76 -0
  50. ophyd_async/epics/pvi.py +0 -70
  51. ophyd_async/panda/panda.py +0 -241
  52. ophyd_async-0.3a1.dist-info/RECORD +0 -56
  53. /ophyd_async/panda/{table.py → _table.py} +0 -0
  54. /ophyd_async/panda/{utils.py → _utils.py} +0 -0
  55. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/LICENSE +0 -0
  56. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/WHEEL +0 -0
  57. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/entry_points.txt +0 -0
  58. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/top_level.txt +0 -0
ophyd_async/core/utils.py CHANGED
@@ -132,7 +132,7 @@ def get_unique(values: Dict[str, T], types: str) -> T:
132
132
 
133
133
 
134
134
  async def merge_gathered_dicts(
135
- coros: Iterable[Awaitable[Dict[str, T]]]
135
+ coros: Iterable[Awaitable[Dict[str, T]]],
136
136
  ) -> Dict[str, T]:
137
137
  """Merge dictionaries produced by a sequence of coroutines.
138
138
 
@@ -148,3 +148,20 @@ async def merge_gathered_dicts(
148
148
 
149
149
  async def gather_list(coros: Iterable[Awaitable[T]]) -> List[T]:
150
150
  return await asyncio.gather(*coros)
151
+
152
+
153
+ def in_micros(t: float) -> int:
154
+ """
155
+ Converts between a positive number of seconds and an equivalent
156
+ number of microseconds.
157
+
158
+ Args:
159
+ t (float): A time in seconds
160
+ Raises:
161
+ ValueError: if t < 0
162
+ Returns:
163
+ t (int): A time in microseconds, rounded up to the nearest whole microsecond,
164
+ """
165
+ if t < 0:
166
+ raise ValueError(f"Expected a positive time in seconds, got {t!r}")
167
+ return int(np.ceil(t * 1e6))
@@ -52,14 +52,14 @@ class CaConverter:
52
52
  return value
53
53
 
54
54
  def reading(self, value: AugmentedValue):
55
- return dict(
56
- value=self.value(value),
57
- timestamp=value.timestamp,
58
- alarm_severity=-1 if value.severity > 2 else value.severity,
59
- )
55
+ return {
56
+ "value": self.value(value),
57
+ "timestamp": value.timestamp,
58
+ "alarm_severity": -1 if value.severity > 2 else value.severity,
59
+ }
60
60
 
61
61
  def descriptor(self, source: str, value: AugmentedValue) -> Descriptor:
62
- return dict(source=source, dtype=dbr_to_dtype[value.datatype], shape=[])
62
+ return {"source": source, "dtype": dbr_to_dtype[value.datatype], "shape": []}
63
63
 
64
64
 
65
65
  class CaLongStrConverter(CaConverter):
@@ -74,7 +74,7 @@ class CaLongStrConverter(CaConverter):
74
74
 
75
75
  class CaArrayConverter(CaConverter):
76
76
  def descriptor(self, source: str, value: AugmentedValue) -> Descriptor:
77
- return dict(source=source, dtype="array", shape=[len(value)])
77
+ return {"source": source, "dtype": "array", "shape": [len(value)]}
78
78
 
79
79
 
80
80
  @dataclass
@@ -92,7 +92,7 @@ class CaEnumConverter(CaConverter):
92
92
 
93
93
  def descriptor(self, source: str, value: AugmentedValue) -> Descriptor:
94
94
  choices = [e.value for e in self.enum_class]
95
- return dict(source=source, dtype="string", shape=[], choices=choices)
95
+ return {"source": source, "dtype": "string", "shape": [], "choices": choices}
96
96
 
97
97
 
98
98
  class DisconnectedCaConverter(CaConverter):
@@ -170,9 +170,11 @@ class CaSignalBackend(SignalBackend[T]):
170
170
  self.write_pv = write_pv
171
171
  self.initial_values: Dict[str, AugmentedValue] = {}
172
172
  self.converter: CaConverter = DisconnectedCaConverter(None, None)
173
- self.source = f"ca://{self.read_pv}"
174
173
  self.subscription: Optional[Subscription] = None
175
174
 
175
+ def source(self, name: str):
176
+ return f"ca://{self.read_pv}"
177
+
176
178
  async def _store_initial_value(self, pv, timeout: float = DEFAULT_TIMEOUT):
177
179
  try:
178
180
  self.initial_values[pv] = await caget(
@@ -216,9 +218,9 @@ class CaSignalBackend(SignalBackend[T]):
216
218
  timeout=None,
217
219
  )
218
220
 
219
- async def get_descriptor(self) -> Descriptor:
221
+ async def get_descriptor(self, source: str) -> Descriptor:
220
222
  value = await self._caget(FORMAT_CTRL)
221
- return self.converter.descriptor(self.source, value)
223
+ return self.converter.descriptor(source, value)
222
224
 
223
225
  async def get_reading(self) -> Reading:
224
226
  value = await self._caget(FORMAT_TIME)
@@ -49,15 +49,15 @@ class PvaConverter:
49
49
  def reading(self, value):
50
50
  ts = value["timeStamp"]
51
51
  sv = value["alarm"]["severity"]
52
- return dict(
53
- value=self.value(value),
54
- timestamp=ts["secondsPastEpoch"] + ts["nanoseconds"] * 1e-9,
55
- alarm_severity=-1 if sv > 2 else sv,
56
- )
52
+ return {
53
+ "value": self.value(value),
54
+ "timestamp": ts["secondsPastEpoch"] + ts["nanoseconds"] * 1e-9,
55
+ "alarm_severity": -1 if sv > 2 else sv,
56
+ }
57
57
 
58
58
  def descriptor(self, source: str, value) -> Descriptor:
59
59
  dtype = specifier_to_dtype[value.type().aspy("value")]
60
- return dict(source=source, dtype=dtype, shape=[])
60
+ return {"source": source, "dtype": dtype, "shape": []}
61
61
 
62
62
  def metadata_fields(self) -> List[str]:
63
63
  """
@@ -74,7 +74,7 @@ class PvaConverter:
74
74
 
75
75
  class PvaArrayConverter(PvaConverter):
76
76
  def descriptor(self, source: str, value) -> Descriptor:
77
- return dict(source=source, dtype="array", shape=[len(value["value"])])
77
+ return {"source": source, "dtype": "array", "shape": [len(value["value"])]}
78
78
 
79
79
 
80
80
  class PvaNDArrayConverter(PvaConverter):
@@ -98,7 +98,7 @@ class PvaNDArrayConverter(PvaConverter):
98
98
 
99
99
  def descriptor(self, source: str, value) -> Descriptor:
100
100
  dims = self._get_dimensions(value)
101
- return dict(source=source, dtype="array", shape=dims)
101
+ return {"source": source, "dtype": "array", "shape": dims}
102
102
 
103
103
  def write_value(self, value):
104
104
  # No clear use-case for writing directly to an NDArray, and some
@@ -122,7 +122,7 @@ class PvaEnumConverter(PvaConverter):
122
122
 
123
123
  def descriptor(self, source: str, value) -> Descriptor:
124
124
  choices = [e.value for e in self.enum_class]
125
- return dict(source=source, dtype="string", shape=[], choices=choices)
125
+ return {"source": source, "dtype": "string", "shape": [], "choices": choices}
126
126
 
127
127
 
128
128
  class PvaEnumBoolConverter(PvaConverter):
@@ -130,7 +130,7 @@ class PvaEnumBoolConverter(PvaConverter):
130
130
  return value["value"]["index"]
131
131
 
132
132
  def descriptor(self, source: str, value) -> Descriptor:
133
- return dict(source=source, dtype="integer", shape=[])
133
+ return {"source": source, "dtype": "integer", "shape": []}
134
134
 
135
135
 
136
136
  class PvaTableConverter(PvaConverter):
@@ -139,7 +139,7 @@ class PvaTableConverter(PvaConverter):
139
139
 
140
140
  def descriptor(self, source: str, value) -> Descriptor:
141
141
  # This is wrong, but defer until we know how to actually describe a table
142
- return dict(source=source, dtype="object", shape=[]) # type: ignore
142
+ return {"source": source, "dtype": "object", "shape": []} # type: ignore
143
143
 
144
144
 
145
145
  class PvaDictConverter(PvaConverter):
@@ -147,7 +147,7 @@ class PvaDictConverter(PvaConverter):
147
147
  ts = time.time()
148
148
  value = value.todict()
149
149
  # Alarm severity is vacuously 0 for a table
150
- return dict(value=value, timestamp=ts, alarm_severity=0)
150
+ return {"value": value, "timestamp": ts, "alarm_severity": 0}
151
151
 
152
152
  def value(self, value: Value):
153
153
  return value.todict()
@@ -236,9 +236,12 @@ class PvaSignalBackend(SignalBackend[T]):
236
236
  self.write_pv = write_pv
237
237
  self.initial_values: Dict[str, Any] = {}
238
238
  self.converter: PvaConverter = DisconnectedPvaConverter()
239
- self.source = f"pva://{self.read_pv}"
240
239
  self.subscription: Optional[Subscription] = None
241
240
 
241
+ @property
242
+ def source(self, name: str):
243
+ return f"pva://{self.read_pv}"
244
+
242
245
  @property
243
246
  def ctxt(self) -> Context:
244
247
  if PvaSignalBackend._ctxt is None:
@@ -279,7 +282,7 @@ class PvaSignalBackend(SignalBackend[T]):
279
282
  write_value = self.initial_values[self.write_pv]
280
283
  else:
281
284
  write_value = self.converter.write_value(value)
282
- coro = self.ctxt.put(self.write_pv, dict(value=write_value), wait=wait)
285
+ coro = self.ctxt.put(self.write_pv, {"value": write_value}, wait=wait)
283
286
  try:
284
287
  await asyncio.wait_for(coro, timeout)
285
288
  except asyncio.TimeoutError as exc:
@@ -290,9 +293,9 @@ class PvaSignalBackend(SignalBackend[T]):
290
293
  )
291
294
  raise NotConnected(f"pva://{self.write_pv}") from exc
292
295
 
293
- async def get_descriptor(self) -> Descriptor:
296
+ async def get_descriptor(self, source: str) -> Descriptor:
294
297
  value = await self.ctxt.get(self.read_pv)
295
- return self.converter.descriptor(self.source, value)
298
+ return self.converter.descriptor(source, value)
296
299
 
297
300
  def _pva_request_string(self, fields: List[str]) -> str:
298
301
  """
@@ -7,14 +7,19 @@ def get_supported_enum_class(
7
7
  datatype: Optional[Type[Enum]],
8
8
  pv_choices: Tuple[Any, ...],
9
9
  ) -> Type[Enum]:
10
- if datatype:
11
- if not issubclass(datatype, Enum):
12
- raise TypeError(f"{pv} has type Enum not {datatype.__name__}")
13
- if not issubclass(datatype, str):
14
- raise TypeError(f"{pv} has type Enum but doesn't inherit from String")
15
- choices = tuple(v.value for v in datatype)
16
- if set(choices).difference(pv_choices):
17
- raise TypeError(f"{pv} has choices {pv_choices}: not all in {choices}")
18
- return Enum(
19
- "GeneratedChoices", {x or "_": x for x in pv_choices}, type=str
20
- ) # type: ignore
10
+ if not datatype:
11
+ return Enum("GeneratedChoices", {x or "_": x for x in pv_choices}, type=str) # type: ignore
12
+
13
+ if not issubclass(datatype, Enum):
14
+ raise TypeError(f"{pv} has type Enum not {datatype.__name__}")
15
+ if not issubclass(datatype, str):
16
+ raise TypeError(f"{pv} has type Enum but doesn't inherit from String")
17
+ choices = tuple(v.value for v in datatype)
18
+ if set(choices) != set(pv_choices):
19
+ raise TypeError(
20
+ (
21
+ f"{pv} has choices {pv_choices}, "
22
+ f"which do not match {datatype}, which has {choices}"
23
+ )
24
+ )
25
+ return datatype
@@ -1,3 +1,5 @@
1
+ from .aravis import AravisDetector
2
+ from .pilatus import PilatusDetector
1
3
  from .single_trigger_det import SingleTriggerDet
2
4
  from .utils import (
3
5
  FileWriteMode,
@@ -9,6 +11,7 @@ from .utils import (
9
11
  )
10
12
 
11
13
  __all__ = [
14
+ "AravisDetector",
12
15
  "SingleTriggerDet",
13
16
  "FileWriteMode",
14
17
  "ImageMode",
@@ -16,4 +19,5 @@ __all__ = [
16
19
  "ad_rw",
17
20
  "NDAttributeDataType",
18
21
  "NDAttributesXML",
22
+ "PilatusDetector",
19
23
  ]
@@ -0,0 +1,69 @@
1
+ from typing import get_args
2
+
3
+ from bluesky.protocols import HasHints, Hints
4
+
5
+ from ophyd_async.core import DirectoryProvider, StandardDetector, TriggerInfo
6
+ from ophyd_async.epics.areadetector.controllers.aravis_controller import (
7
+ AravisController,
8
+ )
9
+ from ophyd_async.epics.areadetector.drivers import ADBaseShapeProvider
10
+ from ophyd_async.epics.areadetector.drivers.aravis_driver import AravisDriver
11
+ from ophyd_async.epics.areadetector.writers import HDFWriter, NDFileHDF
12
+
13
+
14
+ class AravisDetector(StandardDetector, HasHints):
15
+ """
16
+ Ophyd-async implementation of an ADAravis Detector.
17
+ The detector may be configured for an external trigger on a GPIO port,
18
+ which must be done prior to preparing the detector
19
+ """
20
+
21
+ _controller: AravisController
22
+ _writer: HDFWriter
23
+
24
+ def __init__(
25
+ self,
26
+ name: str,
27
+ directory_provider: DirectoryProvider,
28
+ driver: AravisDriver,
29
+ hdf: NDFileHDF,
30
+ gpio_number: AravisController.GPIO_NUMBER = 1,
31
+ **scalar_sigs: str,
32
+ ):
33
+ # Must be child of Detector to pick up connect()
34
+ self.drv = driver
35
+ self.hdf = hdf
36
+
37
+ super().__init__(
38
+ AravisController(self.drv, gpio_number=gpio_number),
39
+ HDFWriter(
40
+ self.hdf,
41
+ directory_provider,
42
+ lambda: self.name,
43
+ ADBaseShapeProvider(self.drv),
44
+ **scalar_sigs,
45
+ ),
46
+ config_sigs=(self.drv.acquire_time, self.drv.acquire),
47
+ name=name,
48
+ )
49
+
50
+ async def _prepare(self, value: TriggerInfo) -> None:
51
+ await self.drv.fetch_deadtime()
52
+ await super()._prepare(value)
53
+
54
+ def get_external_trigger_gpio(self):
55
+ return self._controller.gpio_number
56
+
57
+ def set_external_trigger_gpio(self, gpio_number: AravisController.GPIO_NUMBER):
58
+ supported_gpio_numbers = get_args(AravisController.GPIO_NUMBER)
59
+ if gpio_number not in supported_gpio_numbers:
60
+ raise ValueError(
61
+ f"{self.__class__.__name__} only supports the following GPIO "
62
+ f"indices: {supported_gpio_numbers} but was asked to "
63
+ f"use {gpio_number}"
64
+ )
65
+ self._controller.gpio_number = gpio_number
66
+
67
+ @property
68
+ def hints(self) -> Hints:
69
+ return self._writer.hints
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+ from typing import Literal, Optional, Tuple
3
+
4
+ from ophyd_async.core import (
5
+ AsyncStatus,
6
+ DetectorControl,
7
+ DetectorTrigger,
8
+ set_and_wait_for_value,
9
+ )
10
+ from ophyd_async.epics.areadetector.drivers.aravis_driver import (
11
+ AravisDriver,
12
+ AravisTriggerMode,
13
+ AravisTriggerSource,
14
+ )
15
+ from ophyd_async.epics.areadetector.utils import ImageMode, stop_busy_record
16
+
17
+
18
+ class AravisController(DetectorControl):
19
+ GPIO_NUMBER = Literal[1, 2, 3, 4]
20
+
21
+ def __init__(self, driver: AravisDriver, gpio_number: GPIO_NUMBER) -> None:
22
+ self._drv = driver
23
+ self.gpio_number = gpio_number
24
+
25
+ def get_deadtime(self, exposure: float) -> float:
26
+ return self._drv.dead_time or 0
27
+
28
+ async def arm(
29
+ self,
30
+ num: int = 0,
31
+ trigger: DetectorTrigger = DetectorTrigger.internal,
32
+ exposure: Optional[float] = None,
33
+ ) -> AsyncStatus:
34
+ if num == 0:
35
+ image_mode = ImageMode.continuous
36
+ else:
37
+ image_mode = ImageMode.multiple
38
+ if exposure is not None:
39
+ await self._drv.acquire_time.set(exposure)
40
+
41
+ trigger_mode, trigger_source = self._get_trigger_info(trigger)
42
+ # trigger mode must be set first and on it's own!
43
+ await self._drv.trigger_mode.set(trigger_mode)
44
+
45
+ await asyncio.gather(
46
+ self._drv.trigger_source.set(trigger_source),
47
+ self._drv.num_images.set(num),
48
+ self._drv.image_mode.set(image_mode),
49
+ )
50
+
51
+ status = await set_and_wait_for_value(self._drv.acquire, True)
52
+ return status
53
+
54
+ def _get_trigger_info(
55
+ self, trigger: DetectorTrigger
56
+ ) -> Tuple[AravisTriggerMode, AravisTriggerSource]:
57
+ supported_trigger_types = (
58
+ DetectorTrigger.constant_gate,
59
+ DetectorTrigger.edge_trigger,
60
+ )
61
+ if trigger not in supported_trigger_types:
62
+ raise ValueError(
63
+ f"{self.__class__.__name__} only supports the following trigger "
64
+ f"types: {supported_trigger_types} but was asked to "
65
+ f"use {trigger}"
66
+ )
67
+ if trigger == DetectorTrigger.internal:
68
+ return AravisTriggerMode.off, "Freerun"
69
+ else:
70
+ return (AravisTriggerMode.on, f"Line{self.gpio_number}")
71
+
72
+ async def disarm(self):
73
+ await stop_busy_record(self._drv.acquire, False, timeout=1)
@@ -1,34 +1,36 @@
1
1
  import asyncio
2
- from typing import Optional, Set
2
+ from typing import Optional
3
3
 
4
- from ophyd_async.core import AsyncStatus, DetectorControl, DetectorTrigger
4
+ from ophyd_async.core.async_status import AsyncStatus
5
+ from ophyd_async.core.detector import DetectorControl, DetectorTrigger
5
6
  from ophyd_async.epics.areadetector.drivers.ad_base import (
6
- DEFAULT_GOOD_STATES,
7
- DetectorState,
8
7
  start_acquiring_driver_and_ensure_status,
9
8
  )
10
-
11
- from ..drivers.pilatus_driver import PilatusDriver, TriggerMode
12
- from ..utils import ImageMode, stop_busy_record
13
-
14
- TRIGGER_MODE = {
15
- DetectorTrigger.internal: TriggerMode.internal,
16
- DetectorTrigger.constant_gate: TriggerMode.ext_enable,
17
- DetectorTrigger.variable_gate: TriggerMode.ext_enable,
18
- }
9
+ from ophyd_async.epics.areadetector.drivers.pilatus_driver import (
10
+ PilatusDriver,
11
+ PilatusTriggerMode,
12
+ )
13
+ from ophyd_async.epics.areadetector.utils import ImageMode, stop_busy_record
19
14
 
20
15
 
21
16
  class PilatusController(DetectorControl):
17
+ _supported_trigger_types = {
18
+ DetectorTrigger.internal: PilatusTriggerMode.internal,
19
+ DetectorTrigger.constant_gate: PilatusTriggerMode.ext_enable,
20
+ DetectorTrigger.variable_gate: PilatusTriggerMode.ext_enable,
21
+ }
22
+
22
23
  def __init__(
23
24
  self,
24
25
  driver: PilatusDriver,
25
- good_states: Set[DetectorState] = set(DEFAULT_GOOD_STATES),
26
26
  ) -> None:
27
- self.driver = driver
28
- self.good_states = good_states
27
+ self._drv = driver
29
28
 
30
29
  def get_deadtime(self, exposure: float) -> float:
31
- return 0.001
30
+ # Cite: https://media.dectris.com/User_Manual-PILATUS2-V1_4.pdf
31
+ """The required minimum time difference between ExpPeriod and ExpTime
32
+ (readout time) is 2.28 ms"""
33
+ return 2.28e-3
32
34
 
33
35
  async def arm(
34
36
  self,
@@ -36,14 +38,24 @@ class PilatusController(DetectorControl):
36
38
  trigger: DetectorTrigger = DetectorTrigger.internal,
37
39
  exposure: Optional[float] = None,
38
40
  ) -> AsyncStatus:
41
+ if exposure is not None:
42
+ await self._drv.acquire_time.set(exposure)
39
43
  await asyncio.gather(
40
- self.driver.trigger_mode.set(TRIGGER_MODE[trigger]),
41
- self.driver.num_images.set(999_999 if num == 0 else num),
42
- self.driver.image_mode.set(ImageMode.multiple),
43
- )
44
- return await start_acquiring_driver_and_ensure_status(
45
- self.driver, good_states=self.good_states
44
+ self._drv.trigger_mode.set(self._get_trigger_mode(trigger)),
45
+ self._drv.num_images.set(999_999 if num == 0 else num),
46
+ self._drv.image_mode.set(ImageMode.multiple),
46
47
  )
48
+ return await start_acquiring_driver_and_ensure_status(self._drv)
49
+
50
+ @classmethod
51
+ def _get_trigger_mode(cls, trigger: DetectorTrigger) -> PilatusTriggerMode:
52
+ if trigger not in cls._supported_trigger_types.keys():
53
+ raise ValueError(
54
+ f"{cls.__name__} only supports the following trigger "
55
+ f"types: {cls._supported_trigger_types.keys()} but was asked to "
56
+ f"use {trigger}"
57
+ )
58
+ return cls._supported_trigger_types[trigger]
47
59
 
48
60
  async def disarm(self):
49
- await stop_busy_record(self.driver.acquire, False, timeout=1)
61
+ await stop_busy_record(self._drv.acquire, False, timeout=1)
@@ -0,0 +1,154 @@
1
+ from enum import Enum
2
+ from typing import Callable, Dict, Literal, Optional, Tuple
3
+
4
+ from ophyd_async.epics.areadetector.drivers import ADBase
5
+ from ophyd_async.epics.areadetector.utils import ad_r, ad_rw
6
+
7
+
8
+ class AravisTriggerMode(str, Enum):
9
+ """GigEVision GenICAM standard: on=externally triggered"""
10
+
11
+ on = "On"
12
+ off = "Off"
13
+
14
+
15
+ """A minimal set of TriggerSources that must be supported by the underlying record.
16
+ To enable hardware triggered scanning, line_N must support each N in GPIO_NUMBER.
17
+ To enable software triggered scanning, freerun must be supported.
18
+ Other enumerated values may or may not be preset.
19
+ To prevent requiring one Enum class per possible configuration, we set as this Enum
20
+ but read from the underlying signal as a str.
21
+ """
22
+ AravisTriggerSource = Literal["Freerun", "Line1", "Line2", "Line3", "Line4"]
23
+
24
+
25
+ def _reverse_lookup(
26
+ model_deadtimes: Dict[float, Tuple[str, ...]],
27
+ ) -> Callable[[str], float]:
28
+ def inner(pixel_format: str, model_name: str) -> float:
29
+ for deadtime, pixel_formats in model_deadtimes.items():
30
+ if pixel_format in pixel_formats:
31
+ return deadtime
32
+ raise ValueError(
33
+ f"Model {model_name} does not have a defined deadtime "
34
+ f"for pixel format {pixel_format}"
35
+ )
36
+
37
+ return inner
38
+
39
+
40
+ _deadtimes: Dict[str, Callable[[str, str], float]] = {
41
+ # cite: https://cdn.alliedvision.com/fileadmin/content/documents/products/cameras/Manta/techman/Manta_TechMan.pdf retrieved 2024-04-05 # noqa: E501
42
+ "Manta G-125": lambda _, __: 63e-6,
43
+ "Manta G-145": lambda _, __: 106e-6,
44
+ "Manta G-235": _reverse_lookup(
45
+ {
46
+ 118e-6: (
47
+ "Mono8",
48
+ "Mono12Packed",
49
+ "BayerRG8",
50
+ "BayerRG12",
51
+ "BayerRG12Packed",
52
+ "YUV411Packed",
53
+ ),
54
+ 256e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
55
+ 390e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
56
+ }
57
+ ),
58
+ "Manta G-895": _reverse_lookup(
59
+ {
60
+ 404e-6: (
61
+ "Mono8",
62
+ "Mono12Packed",
63
+ "BayerRG8",
64
+ "BayerRG12Packed",
65
+ "YUV411Packed",
66
+ ),
67
+ 542e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
68
+ 822e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
69
+ }
70
+ ),
71
+ "Manta G-2460": _reverse_lookup(
72
+ {
73
+ 979e-6: (
74
+ "Mono8",
75
+ "Mono12Packed",
76
+ "BayerRG8",
77
+ "BayerRG12Packed",
78
+ "YUV411Packed",
79
+ ),
80
+ 1304e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
81
+ 1961e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
82
+ }
83
+ ),
84
+ # cite: https://cdn.alliedvision.com/fileadmin/content/documents/products/cameras/various/appnote/GigE/GigE-Cameras_AppNote_PIV-Min-Time-Between-Exposures.pdf retrieved 2024-04-05 # noqa: E501
85
+ "Manta G-609": lambda _, __: 47e-6,
86
+ # cite: https://cdn.alliedvision.com/fileadmin/content/documents/products/cameras/Mako/techman/Mako_TechMan_en.pdf retrieved 2024-04-05 # noqa: E501
87
+ "Mako G-040": _reverse_lookup(
88
+ {
89
+ 101e-6: (
90
+ "Mono8",
91
+ "Mono12Packed",
92
+ "BayerRG8",
93
+ "BayerRG12Packed",
94
+ "YUV411Packed",
95
+ ),
96
+ 140e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
97
+ 217e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
98
+ }
99
+ ),
100
+ "Mako G-125": lambda _, __: 70e-6,
101
+ # Assume 12 bits: 10 bits = 275e-6
102
+ "Mako G-234": _reverse_lookup(
103
+ {
104
+ 356e-6: (
105
+ "Mono8",
106
+ "BayerRG8",
107
+ "BayerRG12",
108
+ "BayerRG12Packed",
109
+ "YUV411Packed",
110
+ "YUV422Packed",
111
+ ),
112
+ # Assume 12 bits: 10 bits = 563e-6
113
+ 726e-6: ("RGB8Packed", "BRG8Packed", "YUV444Packed"),
114
+ }
115
+ ),
116
+ "Mako G-507": _reverse_lookup(
117
+ {
118
+ 270e-6: (
119
+ "Mono8",
120
+ "Mono12Packed",
121
+ "BayerRG8",
122
+ "BayerRG12Packed",
123
+ "YUV411Packed",
124
+ ),
125
+ 363e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
126
+ 554e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
127
+ }
128
+ ),
129
+ }
130
+
131
+
132
+ class AravisDriver(ADBase):
133
+ # If instantiating a new instance, ensure it is supported in the _deadtimes dict
134
+ """Generic Driver supporting the Manta and Mako drivers.
135
+ Fetches deadtime prior to use in a Streaming scan.
136
+ Requires driver firmware up to date:
137
+ - Model_RBV must be of the form "^(Mako|Manta) (model)$"
138
+ """
139
+
140
+ def __init__(self, prefix: str, name: str = "") -> None:
141
+ self.trigger_mode = ad_rw(AravisTriggerMode, prefix + "TriggerMode")
142
+ self.trigger_source = ad_rw(str, prefix + "TriggerSource")
143
+ self.model = ad_r(str, prefix + "Model")
144
+ self.pixel_format = ad_rw(str, prefix + "PixelFormat")
145
+ self.dead_time: Optional[float] = None
146
+ super().__init__(prefix, name=name)
147
+
148
+ async def fetch_deadtime(self) -> None:
149
+ # All known in-use version B/C have same deadtime as non-B/C
150
+ model: str = (await self.model.get_value()).removesuffix("B").removesuffix("C")
151
+ if model not in _deadtimes:
152
+ raise ValueError(f"Model {model} does not have defined deadtimes")
153
+ pixel_format: str = await self.pixel_format.get_value()
154
+ self.dead_time = _deadtimes.get(model)(pixel_format, model)
@@ -4,7 +4,7 @@ from ..utils import ad_rw
4
4
  from .ad_base import ADBase
5
5
 
6
6
 
7
- class TriggerMode(str, Enum):
7
+ class PilatusTriggerMode(str, Enum):
8
8
  internal = "Internal"
9
9
  ext_enable = "Ext. Enable"
10
10
  ext_trigger = "Ext. Trigger"
@@ -13,6 +13,6 @@ class TriggerMode(str, Enum):
13
13
 
14
14
 
15
15
  class PilatusDriver(ADBase):
16
- def __init__(self, prefix: str) -> None:
17
- self.trigger_mode = ad_rw(TriggerMode, prefix + "TriggerMode")
18
- super().__init__(prefix)
16
+ def __init__(self, prefix: str, name: str = "") -> None:
17
+ self.trigger_mode = ad_rw(PilatusTriggerMode, prefix + "TriggerMode")
18
+ super().__init__(prefix, name)