ophyd-async 0.8.0a5__py3-none-any.whl → 0.9.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 (116) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +17 -46
  3. ophyd_async/core/_detector.py +68 -44
  4. ophyd_async/core/_device.py +120 -79
  5. ophyd_async/core/_device_filler.py +17 -8
  6. ophyd_async/core/_flyer.py +2 -2
  7. ophyd_async/core/_protocol.py +0 -28
  8. ophyd_async/core/_readable.py +30 -23
  9. ophyd_async/core/_settings.py +104 -0
  10. ophyd_async/core/_signal.py +164 -151
  11. ophyd_async/core/_signal_backend.py +4 -1
  12. ophyd_async/core/_soft_signal_backend.py +2 -1
  13. ophyd_async/core/_table.py +27 -14
  14. ophyd_async/core/_utils.py +30 -5
  15. ophyd_async/core/_yaml_settings.py +64 -0
  16. ophyd_async/epics/adandor/__init__.py +9 -0
  17. ophyd_async/epics/adandor/_andor.py +45 -0
  18. ophyd_async/epics/adandor/_andor_controller.py +49 -0
  19. ophyd_async/epics/adandor/_andor_io.py +36 -0
  20. ophyd_async/epics/adaravis/__init__.py +3 -1
  21. ophyd_async/epics/adaravis/_aravis.py +23 -37
  22. ophyd_async/epics/adaravis/_aravis_controller.py +21 -30
  23. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  24. ophyd_async/epics/adcore/__init__.py +15 -8
  25. ophyd_async/epics/adcore/_core_detector.py +41 -0
  26. ophyd_async/epics/adcore/_core_io.py +56 -31
  27. ophyd_async/epics/adcore/_core_logic.py +99 -84
  28. ophyd_async/epics/adcore/_core_writer.py +219 -0
  29. ophyd_async/epics/adcore/_hdf_writer.py +33 -59
  30. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  31. ophyd_async/epics/adcore/_single_trigger.py +5 -4
  32. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  33. ophyd_async/epics/adcore/_utils.py +37 -36
  34. ophyd_async/epics/adkinetix/_kinetix.py +29 -24
  35. ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
  36. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  37. ophyd_async/epics/adpilatus/__init__.py +2 -2
  38. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  39. ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
  40. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  41. ophyd_async/epics/adsimdetector/__init__.py +3 -3
  42. ophyd_async/epics/adsimdetector/_sim.py +33 -17
  43. ophyd_async/epics/advimba/_vimba.py +23 -23
  44. ophyd_async/epics/advimba/_vimba_controller.py +21 -35
  45. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  46. ophyd_async/epics/core/_aioca.py +52 -21
  47. ophyd_async/epics/core/_p4p.py +59 -16
  48. ophyd_async/epics/core/_pvi_connector.py +4 -2
  49. ophyd_async/epics/core/_signal.py +9 -2
  50. ophyd_async/epics/core/_util.py +10 -1
  51. ophyd_async/epics/eiger/_eiger_controller.py +10 -5
  52. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  53. ophyd_async/epics/motor.py +26 -15
  54. ophyd_async/epics/sim/_ioc.py +29 -0
  55. ophyd_async/epics/{demo → sim}/_mover.py +12 -6
  56. ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
  57. ophyd_async/epics/testing/__init__.py +24 -0
  58. ophyd_async/epics/testing/_example_ioc.py +91 -0
  59. ophyd_async/epics/testing/_utils.py +50 -0
  60. ophyd_async/epics/testing/test_records.db +174 -0
  61. ophyd_async/epics/testing/test_records_pva.db +177 -0
  62. ophyd_async/fastcs/core.py +2 -2
  63. ophyd_async/fastcs/panda/__init__.py +0 -2
  64. ophyd_async/fastcs/panda/_block.py +9 -9
  65. ophyd_async/fastcs/panda/_control.py +9 -4
  66. ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
  67. ophyd_async/fastcs/panda/_table.py +4 -1
  68. ophyd_async/fastcs/panda/_trigger.py +7 -7
  69. ophyd_async/plan_stubs/__init__.py +14 -0
  70. ophyd_async/plan_stubs/_ensure_connected.py +11 -17
  71. ophyd_async/plan_stubs/_fly.py +2 -2
  72. ophyd_async/plan_stubs/_nd_attributes.py +7 -5
  73. ophyd_async/plan_stubs/_panda.py +13 -0
  74. ophyd_async/plan_stubs/_settings.py +125 -0
  75. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  76. ophyd_async/sim/__init__.py +19 -0
  77. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
  78. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
  79. ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
  80. ophyd_async/tango/__init__.py +0 -43
  81. ophyd_async/tango/{signal → core}/__init__.py +7 -2
  82. ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
  83. ophyd_async/tango/{signal → core}/_signal.py +16 -4
  84. ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
  85. ophyd_async/tango/{signal → core}/_tango_transport.py +13 -15
  86. ophyd_async/tango/{demo → sim}/_counter.py +6 -7
  87. ophyd_async/tango/{demo → sim}/_mover.py +13 -9
  88. ophyd_async/testing/__init__.py +52 -0
  89. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  90. ophyd_async/testing/_assert.py +176 -0
  91. ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
  92. ophyd_async/testing/_one_of_everything.py +126 -0
  93. ophyd_async/testing/_wait_for_pending.py +22 -0
  94. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +50 -48
  95. ophyd_async-0.9.0.dist-info/RECORD +129 -0
  96. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/WHEEL +1 -1
  97. ophyd_async/core/_device_save_loader.py +0 -274
  98. ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
  99. ophyd_async/fastcs/panda/_utils.py +0 -16
  100. ophyd_async/sim/demo/__init__.py +0 -19
  101. ophyd_async/sim/testing/__init__.py +0 -0
  102. ophyd_async/tango/base_devices/__init__.py +0 -4
  103. ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
  104. ophyd_async-0.8.0a5.dist-info/entry_points.txt +0 -2
  105. /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
  106. /ophyd_async/epics/{demo → sim}/mover.db +0 -0
  107. /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
  108. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
  109. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
  110. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
  111. /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
  112. /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
  113. /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
  114. /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
  115. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
  116. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  import asyncio
2
+ from enum import Enum
3
+ from typing import TypeVar, get_args
2
4
 
3
5
  from ophyd_async.core import (
4
6
  DEFAULT_TIMEOUT,
5
- AsyncStatus,
6
- DetectorController,
7
7
  DetectorTrigger,
8
8
  TriggerInfo,
9
9
  wait_for_value,
@@ -13,58 +13,83 @@ from ophyd_async.epics import adcore
13
13
  from ._pilatus_io import PilatusDriverIO, PilatusTriggerMode
14
14
 
15
15
 
16
- class PilatusController(DetectorController):
16
+ #: Cite: https://media.dectris.com/User_Manual-PILATUS2-V1_4.pdf
17
+ #: The required minimum time difference between ExpPeriod and ExpTime
18
+ #: (readout time) is 2.28 ms
19
+ #: We provide an option to override for newer Pilatus models
20
+ class PilatusReadoutTime(float, Enum):
21
+ """Pilatus readout time per model in ms"""
22
+
23
+ # Cite: https://media.dectris.com/User_Manual-PILATUS2-V1_4.pdf
24
+ PILATUS2 = 2.28e-3
25
+
26
+ # Cite: https://media.dectris.com/user-manual-pilatus3-2020.pdf
27
+ PILATUS3 = 0.95e-3
28
+
29
+
30
+ PilatusControllerT = TypeVar("PilatusControllerT", bound="PilatusController")
31
+
32
+
33
+ class PilatusController(adcore.ADBaseController[PilatusDriverIO]):
17
34
  _supported_trigger_types = {
18
- DetectorTrigger.internal: PilatusTriggerMode.internal,
19
- DetectorTrigger.constant_gate: PilatusTriggerMode.ext_enable,
20
- DetectorTrigger.variable_gate: PilatusTriggerMode.ext_enable,
35
+ DetectorTrigger.INTERNAL: PilatusTriggerMode.INTERNAL,
36
+ DetectorTrigger.CONSTANT_GATE: PilatusTriggerMode.EXT_ENABLE,
37
+ DetectorTrigger.VARIABLE_GATE: PilatusTriggerMode.EXT_ENABLE,
21
38
  }
22
39
 
23
40
  def __init__(
24
41
  self,
25
42
  driver: PilatusDriverIO,
26
- readout_time: float,
43
+ good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
44
+ readout_time: float = PilatusReadoutTime.PILATUS3,
27
45
  ) -> None:
28
- self._drv = driver
46
+ super().__init__(driver, good_states=good_states)
29
47
  self._readout_time = readout_time
30
- self._arm_status: AsyncStatus | None = None
48
+
49
+ @classmethod
50
+ def controller_and_drv(
51
+ cls: type[PilatusControllerT],
52
+ prefix: str,
53
+ good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
54
+ name: str = "",
55
+ readout_time: float = PilatusReadoutTime.PILATUS3,
56
+ ) -> tuple[PilatusControllerT, PilatusDriverIO]:
57
+ driver_cls = get_args(cls.__orig_bases__[0])[0] # type: ignore
58
+ driver = driver_cls(prefix, name=name)
59
+ controller = cls(driver, good_states=good_states, readout_time=readout_time)
60
+ return controller, driver
31
61
 
32
62
  def get_deadtime(self, exposure: float | None) -> float:
33
63
  return self._readout_time
34
64
 
35
65
  async def prepare(self, trigger_info: TriggerInfo):
36
66
  if trigger_info.livetime is not None:
37
- await adcore.set_exposure_time_and_acquire_period_if_supplied(
38
- self, self._drv, trigger_info.livetime
67
+ await self.set_exposure_time_and_acquire_period_if_supplied(
68
+ trigger_info.livetime
39
69
  )
40
70
  await asyncio.gather(
41
- self._drv.trigger_mode.set(self._get_trigger_mode(trigger_info.trigger)),
42
- self._drv.num_images.set(
71
+ self.driver.trigger_mode.set(self._get_trigger_mode(trigger_info.trigger)),
72
+ self.driver.num_images.set(
43
73
  999_999
44
74
  if trigger_info.total_number_of_triggers == 0
45
75
  else trigger_info.total_number_of_triggers
46
76
  ),
47
- self._drv.image_mode.set(adcore.ImageMode.multiple),
77
+ self.driver.image_mode.set(adcore.ImageMode.MULTIPLE),
48
78
  )
49
79
 
50
80
  async def arm(self):
51
81
  # Standard arm the detector and wait for the acquire PV to be True
52
- self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
53
- self._drv
54
- )
82
+ self._arm_status = await self.start_acquiring_driver_and_ensure_status()
83
+
55
84
  # The pilatus has an additional PV that goes True when the camserver
56
85
  # is actually ready. Should wait for that too or we risk dropping
57
86
  # a frame
58
87
  await wait_for_value(
59
- self._drv.armed,
88
+ self.driver.armed,
60
89
  True,
61
90
  timeout=DEFAULT_TIMEOUT,
62
91
  )
63
92
 
64
- async def wait_for_idle(self):
65
- if self._arm_status:
66
- await self._arm_status
67
-
68
93
  @classmethod
69
94
  def _get_trigger_mode(cls, trigger: DetectorTrigger) -> PilatusTriggerMode:
70
95
  if trigger not in cls._supported_trigger_types.keys():
@@ -74,6 +99,3 @@ class PilatusController(DetectorController):
74
99
  f"use {trigger}"
75
100
  )
76
101
  return cls._supported_trigger_types[trigger]
77
-
78
- async def disarm(self):
79
- await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
@@ -4,11 +4,11 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw_rbv
4
4
 
5
5
 
6
6
  class PilatusTriggerMode(StrictEnum):
7
- internal = "Internal"
8
- ext_enable = "Ext. Enable"
9
- ext_trigger = "Ext. Trigger"
10
- mult_trigger = "Mult. Trigger"
11
- alignment = "Alignment"
7
+ INTERNAL = "Internal"
8
+ EXT_ENABLE = "Ext. Enable"
9
+ EXT_TRIGGER = "Ext. Trigger"
10
+ MULT_TRIGGER = "Mult. Trigger"
11
+ ALIGNMENT = "Alignment"
12
12
 
13
13
 
14
14
  class PilatusDriverIO(adcore.ADBaseIO):
@@ -1,7 +1,7 @@
1
- from ._sim import SimDetector
2
- from ._sim_controller import SimController
1
+ from ._sim import SimController, SimDetector, SimDriverIO
3
2
 
4
3
  __all__ = [
5
- "SimDetector",
4
+ "SimDriverIO",
6
5
  "SimController",
6
+ "SimDetector",
7
7
  ]
@@ -1,35 +1,51 @@
1
1
  from collections.abc import Sequence
2
2
 
3
- from ophyd_async.core import PathProvider, SignalR, StandardDetector
3
+ from ophyd_async.core import PathProvider, SignalR
4
4
  from ophyd_async.epics import adcore
5
5
 
6
- from ._sim_controller import SimController
7
6
 
7
+ class SimDriverIO(adcore.ADBaseIO): ...
8
8
 
9
- class SimDetector(StandardDetector):
10
- _controller: SimController
11
- _writer: adcore.ADHDFWriter
12
9
 
10
+ class SimController(adcore.ADBaseController[SimDriverIO]):
11
+ def __init__(
12
+ self,
13
+ driver: SimDriverIO,
14
+ good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
15
+ ) -> None:
16
+ super().__init__(driver, good_states=good_states)
17
+
18
+ def get_deadtime(self, exposure: float | None) -> float:
19
+ return 0.001
20
+
21
+
22
+ class SimDetector(adcore.AreaDetector[SimController]):
13
23
  def __init__(
14
24
  self,
15
25
  prefix: str,
16
26
  path_provider: PathProvider,
17
27
  drv_suffix="cam1:",
18
- hdf_suffix="HDF1:",
19
- name: str = "",
28
+ writer_cls: type[adcore.ADWriter] = adcore.ADHDFWriter,
29
+ fileio_suffix: str | None = None,
30
+ name="",
20
31
  config_sigs: Sequence[SignalR] = (),
32
+ plugins: dict[str, adcore.NDPluginBaseIO] | None = None,
21
33
  ):
22
- self.drv = adcore.ADBaseIO(prefix + drv_suffix)
23
- self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix)
34
+ driver = SimDriverIO(prefix + drv_suffix)
35
+ controller = SimController(driver)
36
+
37
+ writer = writer_cls.with_io(
38
+ prefix,
39
+ path_provider,
40
+ dataset_source=driver,
41
+ fileio_suffix=fileio_suffix,
42
+ plugins=plugins,
43
+ )
24
44
 
25
45
  super().__init__(
26
- SimController(self.drv),
27
- adcore.ADHDFWriter(
28
- self.hdf,
29
- path_provider,
30
- lambda: self.name,
31
- adcore.ADBaseDatasetDescriber(self.drv),
32
- ),
33
- config_sigs=(self.drv.acquire_period, self.drv.acquire_time, *config_sigs),
46
+ controller=controller,
47
+ writer=writer,
48
+ plugins=plugins,
34
49
  name=name,
50
+ config_sigs=config_sigs,
35
51
  )
@@ -1,43 +1,43 @@
1
- from bluesky.protocols import HasHints, Hints
1
+ from collections.abc import Sequence
2
2
 
3
- from ophyd_async.core import PathProvider, StandardDetector
3
+ from ophyd_async.core import PathProvider, SignalR
4
4
  from ophyd_async.epics import adcore
5
5
 
6
6
  from ._vimba_controller import VimbaController
7
7
  from ._vimba_io import VimbaDriverIO
8
8
 
9
9
 
10
- class VimbaDetector(StandardDetector, HasHints):
10
+ class VimbaDetector(adcore.AreaDetector[VimbaController]):
11
11
  """
12
12
  Ophyd-async implementation of an ADVimba Detector.
13
13
  """
14
14
 
15
- _controller: VimbaController
16
- _writer: adcore.ADHDFWriter
17
-
18
15
  def __init__(
19
16
  self,
20
17
  prefix: str,
21
18
  path_provider: PathProvider,
22
- drv_suffix="cam1:",
23
- hdf_suffix="HDF1:",
24
- name="",
19
+ drv_suffix: str = "cam1:",
20
+ writer_cls: type[adcore.ADWriter] = adcore.ADHDFWriter,
21
+ fileio_suffix: str | None = None,
22
+ name: str = "",
23
+ plugins: dict[str, adcore.NDPluginBaseIO] | None = None,
24
+ config_sigs: Sequence[SignalR] = (),
25
25
  ):
26
- self.drv = VimbaDriverIO(prefix + drv_suffix)
27
- self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix)
26
+ driver = VimbaDriverIO(prefix + drv_suffix)
27
+ controller = VimbaController(driver)
28
+
29
+ writer = writer_cls.with_io(
30
+ prefix,
31
+ path_provider,
32
+ dataset_source=driver,
33
+ fileio_suffix=fileio_suffix,
34
+ plugins=plugins,
35
+ )
28
36
 
29
37
  super().__init__(
30
- VimbaController(self.drv),
31
- adcore.ADHDFWriter(
32
- self.hdf,
33
- path_provider,
34
- lambda: self.name,
35
- adcore.ADBaseDatasetDescriber(self.drv),
36
- ),
37
- config_sigs=(self.drv.acquire_time,),
38
+ controller=controller,
39
+ writer=writer,
40
+ plugins=plugins,
38
41
  name=name,
42
+ config_sigs=config_sigs,
39
43
  )
40
-
41
- @property
42
- def hints(self) -> Hints:
43
- return self._writer.hints
@@ -1,8 +1,6 @@
1
1
  import asyncio
2
2
 
3
3
  from ophyd_async.core import (
4
- AsyncStatus,
5
- DetectorController,
6
4
  DetectorTrigger,
7
5
  TriggerInfo,
8
6
  )
@@ -11,56 +9,44 @@ from ophyd_async.epics import adcore
11
9
  from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
12
10
 
13
11
  TRIGGER_MODE = {
14
- DetectorTrigger.internal: VimbaOnOff.off,
15
- DetectorTrigger.constant_gate: VimbaOnOff.on,
16
- DetectorTrigger.variable_gate: VimbaOnOff.on,
17
- DetectorTrigger.edge_trigger: VimbaOnOff.on,
12
+ DetectorTrigger.INTERNAL: VimbaOnOff.OFF,
13
+ DetectorTrigger.CONSTANT_GATE: VimbaOnOff.ON,
14
+ DetectorTrigger.VARIABLE_GATE: VimbaOnOff.ON,
15
+ DetectorTrigger.EDGE_TRIGGER: VimbaOnOff.ON,
18
16
  }
19
17
 
20
18
  EXPOSE_OUT_MODE = {
21
- DetectorTrigger.internal: VimbaExposeOutMode.timed,
22
- DetectorTrigger.constant_gate: VimbaExposeOutMode.trigger_width,
23
- DetectorTrigger.variable_gate: VimbaExposeOutMode.trigger_width,
24
- DetectorTrigger.edge_trigger: VimbaExposeOutMode.timed,
19
+ DetectorTrigger.INTERNAL: VimbaExposeOutMode.TIMED,
20
+ DetectorTrigger.CONSTANT_GATE: VimbaExposeOutMode.TRIGGER_WIDTH,
21
+ DetectorTrigger.VARIABLE_GATE: VimbaExposeOutMode.TRIGGER_WIDTH,
22
+ DetectorTrigger.EDGE_TRIGGER: VimbaExposeOutMode.TIMED,
25
23
  }
26
24
 
27
25
 
28
- class VimbaController(DetectorController):
26
+ class VimbaController(adcore.ADBaseController[VimbaDriverIO]):
29
27
  def __init__(
30
28
  self,
31
29
  driver: VimbaDriverIO,
30
+ good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
32
31
  ) -> None:
33
- self._drv = driver
34
- self._arm_status: AsyncStatus | None = None
32
+ super().__init__(driver, good_states=good_states)
35
33
 
36
34
  def get_deadtime(self, exposure: float | None) -> float:
37
35
  return 0.001
38
36
 
39
37
  async def prepare(self, trigger_info: TriggerInfo):
40
38
  await asyncio.gather(
41
- self._drv.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
42
- self._drv.exposure_mode.set(EXPOSE_OUT_MODE[trigger_info.trigger]),
43
- self._drv.num_images.set(trigger_info.total_number_of_triggers),
44
- self._drv.image_mode.set(adcore.ImageMode.multiple),
39
+ self.driver.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
40
+ self.driver.exposure_mode.set(EXPOSE_OUT_MODE[trigger_info.trigger]),
41
+ self.driver.num_images.set(trigger_info.total_number_of_triggers),
42
+ self.driver.image_mode.set(adcore.ImageMode.MULTIPLE),
45
43
  )
46
44
  if trigger_info.livetime is not None and trigger_info.trigger not in [
47
- DetectorTrigger.variable_gate,
48
- DetectorTrigger.constant_gate,
45
+ DetectorTrigger.VARIABLE_GATE,
46
+ DetectorTrigger.CONSTANT_GATE,
49
47
  ]:
50
- await self._drv.acquire_time.set(trigger_info.livetime)
51
- if trigger_info.trigger != DetectorTrigger.internal:
52
- self._drv.trigger_source.set(VimbaTriggerSource.line1)
48
+ await self.driver.acquire_time.set(trigger_info.livetime)
49
+ if trigger_info.trigger != DetectorTrigger.INTERNAL:
50
+ self.driver.trigger_source.set(VimbaTriggerSource.LINE1)
53
51
  else:
54
- self._drv.trigger_source.set(VimbaTriggerSource.freerun)
55
-
56
- async def arm(self):
57
- self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
58
- self._drv
59
- )
60
-
61
- async def wait_for_idle(self):
62
- if self._arm_status:
63
- await self._arm_status
64
-
65
- async def disarm(self):
66
- await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
52
+ self.driver.trigger_source.set(VimbaTriggerSource.FREERUN)
@@ -4,44 +4,44 @@ from ophyd_async.epics.core import epics_signal_rw_rbv
4
4
 
5
5
 
6
6
  class VimbaPixelFormat(StrictEnum):
7
- internal = "Mono8"
8
- ext_enable = "Mono12"
9
- ext_trigger = "Ext. Trigger"
10
- mult_trigger = "Mult. Trigger"
11
- alignment = "Alignment"
7
+ INTERNAL = "Mono8"
8
+ EXT_ENABLE = "Mono12"
9
+ EXT_TRIGGER = "Ext. Trigger"
10
+ MULT_TRIGGER = "Mult. Trigger"
11
+ ALIGNMENT = "Alignment"
12
12
 
13
13
 
14
14
  class VimbaConvertFormat(StrictEnum):
15
- none = "None"
16
- mono8 = "Mono8"
17
- mono16 = "Mono16"
18
- rgb8 = "RGB8"
19
- rgb16 = "RGB16"
15
+ NONE = "None"
16
+ MONO8 = "Mono8"
17
+ MONO16 = "Mono16"
18
+ RGB8 = "RGB8"
19
+ RGB16 = "RGB16"
20
20
 
21
21
 
22
22
  class VimbaTriggerSource(StrictEnum):
23
- freerun = "Freerun"
24
- line1 = "Line1"
25
- line2 = "Line2"
26
- fixed_rate = "FixedRate"
27
- software = "Software"
28
- action0 = "Action0"
29
- action1 = "Action1"
23
+ FREERUN = "Freerun"
24
+ LINE1 = "Line1"
25
+ LINE2 = "Line2"
26
+ FIXED_RATE = "FixedRate"
27
+ SOFTWARE = "Software"
28
+ ACTION0 = "Action0"
29
+ ACTION1 = "Action1"
30
30
 
31
31
 
32
32
  class VimbaOverlap(StrictEnum):
33
- off = "Off"
34
- prev_frame = "PreviousFrame"
33
+ OFF = "Off"
34
+ PREV_FRAME = "PreviousFrame"
35
35
 
36
36
 
37
37
  class VimbaOnOff(StrictEnum):
38
- on = "On"
39
- off = "Off"
38
+ ON = "On"
39
+ OFF = "Off"
40
40
 
41
41
 
42
42
  class VimbaExposeOutMode(StrictEnum):
43
- timed = "Timed" # Use ExposureTime PV
44
- trigger_width = "TriggerWidth" # Expose for length of high signal
43
+ TIMED = "Timed" # Use ExposureTime PV
44
+ TRIGGER_WIDTH = "TriggerWidth" # Expose for length of high signal
45
45
 
46
46
 
47
47
  class VimbaDriverIO(adcore.ADBaseIO):
@@ -35,12 +35,14 @@ from ophyd_async.core import (
35
35
 
36
36
  from ._util import EpicsSignalBackend, format_datatype, get_supported_values
37
37
 
38
+ logger = logging.getLogger("ophyd_async")
39
+
38
40
 
39
41
  def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
40
42
  def get_limits(limit: str) -> LimitsRange | None:
41
43
  low = getattr(value, f"lower_{limit}_limit", nan)
42
44
  high = getattr(value, f"upper_{limit}_limit", nan)
43
- if not (isnan(low) and isnan(high)):
45
+ if not (isnan(low) and isnan(high)) and not high == low == 0:
44
46
  return LimitsRange(
45
47
  low=None if isnan(low) else low,
46
48
  high=None if isnan(high) else high,
@@ -59,14 +61,20 @@ def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
59
61
 
60
62
 
61
63
  def _metadata_from_augmented_value(
62
- value: AugmentedValue, metadata: SignalMetadata
64
+ datatype: type[SignalDatatypeT] | None,
65
+ value: AugmentedValue,
66
+ metadata: SignalMetadata,
63
67
  ) -> SignalMetadata:
64
68
  metadata = metadata.copy()
65
- if hasattr(value, "units"):
69
+ if hasattr(value, "units") and datatype not in (str, bool):
66
70
  metadata["units"] = value.units
67
- if hasattr(value, "precision") and not isnan(value.precision):
71
+ if (
72
+ hasattr(value, "precision")
73
+ and not isnan(value.precision)
74
+ and datatype is not int
75
+ ):
68
76
  metadata["precision"] = value.precision
69
- if limits := _limits_from_augmented_value(value):
77
+ if (limits := _limits_from_augmented_value(value)) and datatype is not bool:
70
78
  metadata["limits"] = limits
71
79
  return metadata
72
80
 
@@ -100,6 +108,11 @@ class DisconnectedCaConverter(CaConverter):
100
108
  raise NotImplementedError("No PV has been set as connect() has not been called")
101
109
 
102
110
 
111
+ class CaIntConverter(CaConverter[int]):
112
+ def value(self, value: AugmentedValue) -> int:
113
+ return int(value) # type: ignore
114
+
115
+
103
116
  class CaArrayConverter(CaConverter[np.ndarray]):
104
117
  def value(self, value: AugmentedValue) -> np.ndarray:
105
118
  # A less expensive conversion
@@ -202,7 +215,7 @@ def make_converter(
202
215
  and get_unique({k: v.precision for k, v in values.items()}, "precision") == 0
203
216
  ):
204
217
  # Allow int signals to represent float records when prec is 0
205
- return CaConverter(int, pv_dbr)
218
+ return CaIntConverter(int, pv_dbr)
206
219
  elif datatype in (None, inferred_datatype):
207
220
  # If datatype matches what we are given then allow it and use inferred converter
208
221
  return converter_cls(inferred_datatype, pv_dbr)
@@ -247,7 +260,7 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
247
260
  pv, format=FORMAT_CTRL, timeout=timeout
248
261
  )
249
262
  except CANothing as exc:
250
- logging.debug(f"signal ca://{pv} timed out")
263
+ logger.debug(f"signal ca://{pv} timed out")
251
264
  raise NotConnected(f"ca://{pv}") from exc
252
265
 
253
266
  async def connect(self, timeout: float):
@@ -280,17 +293,33 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
280
293
  write_value = self.initial_values[self.write_pv]
281
294
  else:
282
295
  write_value = self.converter.write_value(value)
283
- await caput(
284
- self.write_pv,
285
- write_value,
286
- datatype=self.converter.write_dbr,
287
- wait=wait,
288
- timeout=None,
289
- )
296
+ try:
297
+ await caput(
298
+ self.write_pv,
299
+ write_value,
300
+ datatype=self.converter.write_dbr,
301
+ wait=wait,
302
+ timeout=None,
303
+ )
304
+ except CANothing as exc:
305
+ # If we ran into a write error, check to see if there is a list
306
+ # of valid choices, and if the value we tried to write is in that list.
307
+ valid_choices = self.converter.metadata.get("choices")
308
+ if valid_choices:
309
+ if value not in valid_choices:
310
+ msg = (
311
+ f"{value} is not a valid choice for {self.write_pv}, "
312
+ f"valid choices: {self.converter.metadata.get('choices')}"
313
+ )
314
+ raise ValueError(msg) from exc
315
+ raise
316
+ raise
290
317
 
291
318
  async def get_datakey(self, source: str) -> DataKey:
292
319
  value = await self._caget(self.read_pv, FORMAT_CTRL)
293
- metadata = _metadata_from_augmented_value(value, self.converter.metadata)
320
+ metadata = _metadata_from_augmented_value(
321
+ self.datatype, value, self.converter.metadata
322
+ )
294
323
  return make_datakey(
295
324
  self.converter.datatype, self.converter.value(value), source, metadata
296
325
  )
@@ -308,16 +337,18 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
308
337
  return self.converter.value(value)
309
338
 
310
339
  def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
340
+ if callback and self.subscription:
341
+ msg = "Cannot set a callback when one is already set"
342
+ raise RuntimeError(msg)
343
+
344
+ if self.subscription:
345
+ self.subscription.close()
346
+ self.subscription = None
347
+
311
348
  if callback:
312
- assert (
313
- not self.subscription
314
- ), "Cannot set a callback when one is already set"
315
349
  self.subscription = camonitor(
316
350
  self.read_pv,
317
351
  lambda v: callback(self._make_reading(v)),
318
352
  datatype=self.converter.read_dbr,
319
353
  format=FORMAT_TIME,
320
354
  )
321
- elif self.subscription:
322
- self.subscription.close()
323
- self.subscription = None