ophyd-async 0.9.0a1__py3-none-any.whl → 0.9.0a2__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 (97) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +13 -20
  3. ophyd_async/core/_detector.py +61 -37
  4. ophyd_async/core/_device.py +102 -80
  5. ophyd_async/core/_device_filler.py +17 -8
  6. ophyd_async/core/_flyer.py +2 -2
  7. ophyd_async/core/_readable.py +30 -23
  8. ophyd_async/core/_settings.py +104 -0
  9. ophyd_async/core/_signal.py +55 -17
  10. ophyd_async/core/_signal_backend.py +4 -1
  11. ophyd_async/core/_soft_signal_backend.py +2 -1
  12. ophyd_async/core/_table.py +18 -10
  13. ophyd_async/core/_utils.py +5 -3
  14. ophyd_async/core/_yaml_settings.py +64 -0
  15. ophyd_async/epics/adandor/__init__.py +9 -0
  16. ophyd_async/epics/adandor/_andor.py +45 -0
  17. ophyd_async/epics/adandor/_andor_controller.py +49 -0
  18. ophyd_async/epics/adandor/_andor_io.py +36 -0
  19. ophyd_async/epics/adaravis/__init__.py +3 -1
  20. ophyd_async/epics/adaravis/_aravis.py +23 -37
  21. ophyd_async/epics/adaravis/_aravis_controller.py +13 -22
  22. ophyd_async/epics/adcore/__init__.py +15 -8
  23. ophyd_async/epics/adcore/_core_detector.py +41 -0
  24. ophyd_async/epics/adcore/_core_io.py +35 -10
  25. ophyd_async/epics/adcore/_core_logic.py +98 -86
  26. ophyd_async/epics/adcore/_core_writer.py +219 -0
  27. ophyd_async/epics/adcore/_hdf_writer.py +38 -62
  28. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  29. ophyd_async/epics/adcore/_single_trigger.py +4 -3
  30. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  31. ophyd_async/epics/adcore/_utils.py +2 -1
  32. ophyd_async/epics/adkinetix/_kinetix.py +29 -24
  33. ophyd_async/epics/adkinetix/_kinetix_controller.py +9 -21
  34. ophyd_async/epics/adpilatus/__init__.py +2 -2
  35. ophyd_async/epics/adpilatus/_pilatus.py +27 -39
  36. ophyd_async/epics/adpilatus/_pilatus_controller.py +44 -22
  37. ophyd_async/epics/adsimdetector/__init__.py +3 -3
  38. ophyd_async/epics/adsimdetector/_sim.py +33 -17
  39. ophyd_async/epics/advimba/_vimba.py +23 -23
  40. ophyd_async/epics/advimba/_vimba_controller.py +10 -24
  41. ophyd_async/epics/core/_aioca.py +31 -14
  42. ophyd_async/epics/core/_p4p.py +40 -16
  43. ophyd_async/epics/core/_util.py +1 -1
  44. ophyd_async/epics/motor.py +18 -10
  45. ophyd_async/epics/sim/_ioc.py +29 -0
  46. ophyd_async/epics/{demo → sim}/_mover.py +10 -4
  47. ophyd_async/epics/testing/__init__.py +14 -14
  48. ophyd_async/epics/testing/_example_ioc.py +48 -65
  49. ophyd_async/epics/testing/_utils.py +17 -45
  50. ophyd_async/epics/testing/test_records.db +8 -0
  51. ophyd_async/fastcs/panda/__init__.py +0 -2
  52. ophyd_async/fastcs/panda/_control.py +7 -2
  53. ophyd_async/fastcs/panda/_hdf_panda.py +3 -1
  54. ophyd_async/fastcs/panda/_table.py +4 -1
  55. ophyd_async/plan_stubs/__init__.py +14 -0
  56. ophyd_async/plan_stubs/_ensure_connected.py +11 -17
  57. ophyd_async/plan_stubs/_fly.py +1 -1
  58. ophyd_async/plan_stubs/_nd_attributes.py +7 -5
  59. ophyd_async/plan_stubs/_panda.py +13 -0
  60. ophyd_async/plan_stubs/_settings.py +125 -0
  61. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  62. ophyd_async/sim/__init__.py +19 -0
  63. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
  64. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
  65. ophyd_async/tango/core/_signal.py +3 -1
  66. ophyd_async/tango/core/_tango_transport.py +12 -14
  67. ophyd_async/tango/{demo → sim}/_mover.py +5 -2
  68. ophyd_async/testing/__init__.py +19 -0
  69. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  70. ophyd_async/testing/_assert.py +88 -40
  71. ophyd_async/testing/_mock_signal_utils.py +3 -3
  72. ophyd_async/testing/_one_of_everything.py +126 -0
  73. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/METADATA +2 -2
  74. ophyd_async-0.9.0a2.dist-info/RECORD +129 -0
  75. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/WHEEL +1 -1
  76. ophyd_async/core/_device_save_loader.py +0 -274
  77. ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
  78. ophyd_async/fastcs/panda/_utils.py +0 -16
  79. ophyd_async/sim/demo/__init__.py +0 -19
  80. ophyd_async/sim/testing/__init__.py +0 -0
  81. ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
  82. ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
  83. /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
  84. /ophyd_async/epics/{demo → sim}/_sensor.py +0 -0
  85. /ophyd_async/epics/{demo → sim}/mover.db +0 -0
  86. /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
  87. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
  88. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
  89. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
  90. /ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +0 -0
  91. /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
  92. /ophyd_async/tango/{demo → sim}/_counter.py +0 -0
  93. /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
  94. /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
  95. /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
  96. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/LICENSE +0 -0
  97. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/top_level.txt +0 -0
@@ -1,58 +1,46 @@
1
- from enum import Enum
1
+ from collections.abc import Sequence
2
2
 
3
- from bluesky.protocols import Hints
3
+ from ophyd_async.core import PathProvider
4
+ from ophyd_async.core._signal import SignalR
5
+ from ophyd_async.epics.adcore._core_detector import AreaDetector
6
+ from ophyd_async.epics.adcore._core_io import NDPluginBaseIO
7
+ from ophyd_async.epics.adcore._core_writer import ADWriter
8
+ from ophyd_async.epics.adcore._hdf_writer import ADHDFWriter
4
9
 
5
- from ophyd_async.core import PathProvider, StandardDetector
6
- from ophyd_async.epics import adcore
7
-
8
- from ._pilatus_controller import PilatusController
10
+ from ._pilatus_controller import PilatusController, PilatusReadoutTime
9
11
  from ._pilatus_io import PilatusDriverIO
10
12
 
11
13
 
12
- #: Cite: https://media.dectris.com/User_Manual-PILATUS2-V1_4.pdf
13
- #: The required minimum time difference between ExpPeriod and ExpTime
14
- #: (readout time) is 2.28 ms
15
- #: We provide an option to override for newer Pilatus models
16
- class PilatusReadoutTime(float, Enum):
17
- """Pilatus readout time per model in ms"""
18
-
19
- # Cite: https://media.dectris.com/User_Manual-PILATUS2-V1_4.pdf
20
- PILATUS2 = 2.28e-3
21
-
22
- # Cite: https://media.dectris.com/user-manual-pilatus3-2020.pdf
23
- PILATUS3 = 0.95e-3
24
-
25
-
26
- class PilatusDetector(StandardDetector):
14
+ class PilatusDetector(AreaDetector[PilatusController]):
27
15
  """A Pilatus StandardDetector writing HDF files"""
28
16
 
29
- _controller: PilatusController
30
- _writer: adcore.ADHDFWriter
31
-
32
17
  def __init__(
33
18
  self,
34
19
  prefix: str,
35
20
  path_provider: PathProvider,
36
21
  readout_time: PilatusReadoutTime = PilatusReadoutTime.PILATUS3,
37
22
  drv_suffix: str = "cam1:",
38
- hdf_suffix: str = "HDF1:",
23
+ writer_cls: type[ADWriter] = ADHDFWriter,
24
+ fileio_suffix: str | None = None,
39
25
  name: str = "",
26
+ plugins: dict[str, NDPluginBaseIO] | None = None,
27
+ config_sigs: Sequence[SignalR] = (),
40
28
  ):
41
- self.drv = PilatusDriverIO(prefix + drv_suffix)
42
- self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix)
29
+ driver = PilatusDriverIO(prefix + drv_suffix)
30
+ controller = PilatusController(driver)
31
+
32
+ writer = writer_cls.with_io(
33
+ prefix,
34
+ path_provider,
35
+ dataset_source=driver,
36
+ fileio_suffix=fileio_suffix,
37
+ plugins=plugins,
38
+ )
43
39
 
44
40
  super().__init__(
45
- PilatusController(self.drv, readout_time=readout_time.value),
46
- adcore.ADHDFWriter(
47
- self.hdf,
48
- path_provider,
49
- lambda: self.name,
50
- adcore.ADBaseDatasetDescriber(self.drv),
51
- ),
52
- config_sigs=(self.drv.acquire_time,),
41
+ controller=controller,
42
+ writer=writer,
43
+ plugins=plugins,
53
44
  name=name,
45
+ config_sigs=config_sigs,
54
46
  )
55
-
56
- @property
57
- def hints(self) -> Hints:
58
- return self._writer.hints
@@ -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,7 +13,24 @@ 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
35
  DetectorTrigger.INTERNAL: PilatusTriggerMode.INTERNAL,
19
36
  DetectorTrigger.CONSTANT_GATE: PilatusTriggerMode.EXT_ENABLE,
@@ -23,48 +40,56 @@ class PilatusController(DetectorController):
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)
@@ -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
  )
@@ -25,42 +23,30 @@ EXPOSE_OUT_MODE = {
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
45
  DetectorTrigger.VARIABLE_GATE,
48
46
  DetectorTrigger.CONSTANT_GATE,
49
47
  ]:
50
- await self._drv.acquire_time.set(trigger_info.livetime)
48
+ await self.driver.acquire_time.set(trigger_info.livetime)
51
49
  if trigger_info.trigger != DetectorTrigger.INTERNAL:
52
- self._drv.trigger_source.set(VimbaTriggerSource.LINE1)
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)
@@ -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):
@@ -290,7 +303,9 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
290
303
 
291
304
  async def get_datakey(self, source: str) -> DataKey:
292
305
  value = await self._caget(self.read_pv, FORMAT_CTRL)
293
- metadata = _metadata_from_augmented_value(value, self.converter.metadata)
306
+ metadata = _metadata_from_augmented_value(
307
+ self.datatype, value, self.converter.metadata
308
+ )
294
309
  return make_datakey(
295
310
  self.converter.datatype, self.converter.value(value), source, metadata
296
311
  )
@@ -308,16 +323,18 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
308
323
  return self.converter.value(value)
309
324
 
310
325
  def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
326
+ if callback and self.subscription:
327
+ msg = "Cannot set a callback when one is already set"
328
+ raise RuntimeError(msg)
329
+
330
+ if self.subscription:
331
+ self.subscription.close()
332
+ self.subscription = None
333
+
311
334
  if callback:
312
- assert (
313
- not self.subscription
314
- ), "Cannot set a callback when one is already set"
315
335
  self.subscription = camonitor(
316
336
  self.read_pv,
317
337
  lambda v: callback(self._make_reading(v)),
318
338
  datatype=self.converter.read_dbr,
319
339
  format=FORMAT_TIME,
320
340
  )
321
- elif self.subscription:
322
- self.subscription.close()
323
- self.subscription = None
@@ -31,6 +31,8 @@ from ophyd_async.core import (
31
31
 
32
32
  from ._util import EpicsSignalBackend, format_datatype, get_supported_values
33
33
 
34
+ logger = logging.getLogger("ophyd_async")
35
+
34
36
 
35
37
  def _limits_from_value(value: Any) -> Limits:
36
38
  def get_limits(
@@ -39,7 +41,7 @@ def _limits_from_value(value: Any) -> Limits:
39
41
  substructure = getattr(value, substucture_name, None)
40
42
  low = getattr(substructure, low_name, nan)
41
43
  high = getattr(substructure, high_name, nan)
42
- if not (isnan(low) and isnan(high)):
44
+ if not (isnan(low) and isnan(high)) and not low == high == 0:
43
45
  return LimitsRange(
44
46
  low=None if isnan(low) else low,
45
47
  high=None if isnan(high) else high,
@@ -60,12 +62,22 @@ def _limits_from_value(value: Any) -> Limits:
60
62
  def _metadata_from_value(datatype: type[SignalDatatype], value: Any) -> SignalMetadata:
61
63
  metadata = SignalMetadata()
62
64
  value_data: Any = getattr(value, "value", None)
65
+ specifier = _get_specifier(value)
63
66
  display_data: Any = getattr(value, "display", None)
64
- if hasattr(display_data, "units"):
67
+ if (
68
+ hasattr(display_data, "units")
69
+ and specifier[-1] in _number_specifiers
70
+ and datatype is not str
71
+ ):
65
72
  metadata["units"] = display_data.units
66
- if hasattr(display_data, "precision") and not isnan(display_data.precision):
73
+ if (
74
+ hasattr(display_data, "precision")
75
+ and not isnan(display_data.precision)
76
+ and specifier[-1] in _float_specifiers
77
+ and datatype is not int
78
+ ):
67
79
  metadata["precision"] = display_data.precision
68
- if limits := _limits_from_value(value):
80
+ if (limits := _limits_from_value(value)) and specifier[-1] in _number_specifiers:
69
81
  metadata["limits"] = limits
70
82
  # Get choices from display or value
71
83
  if datatype is str or issubclass(datatype, StrictEnum):
@@ -84,9 +96,7 @@ class PvaConverter(Generic[SignalDatatypeT]):
84
96
  self.datatype = datatype
85
97
 
86
98
  def value(self, value: Any) -> SignalDatatypeT:
87
- # for channel access ca_xxx classes, this
88
- # invokes __pos__ operator to return an instance of
89
- # the builtin base class
99
+ # Normally the value will be of the correct python type
90
100
  return value["value"]
91
101
 
92
102
  def write_value(self, value: Any) -> Any:
@@ -94,6 +104,15 @@ class PvaConverter(Generic[SignalDatatypeT]):
94
104
  return value
95
105
 
96
106
 
107
+ class PvaIntConverter(PvaConverter[int]):
108
+ def __init__(self):
109
+ super().__init__(int)
110
+
111
+ def value(self, value: Any) -> int:
112
+ # Convert to an int
113
+ return int(value["value"])
114
+
115
+
97
116
  class PvaLongStringConverter(PvaConverter[str]):
98
117
  def __init__(self):
99
118
  super().__init__(str)
@@ -174,6 +193,9 @@ class PvaTableConverter(PvaConverter[Table]):
174
193
 
175
194
 
176
195
  # https://mdavidsaver.github.io/p4p/values.html
196
+ _float_specifiers = {"f", "d"}
197
+ _int_specifiers = {"b", "B", "h", "H", "i", "I", "l", "L"}
198
+ _number_specifiers = _float_specifiers.union(_int_specifiers)
177
199
  _datatype_converter_from_typeid: dict[
178
200
  tuple[str, str], tuple[type[SignalDatatype], type[PvaConverter]]
179
201
  ] = {
@@ -208,7 +230,7 @@ _datatype_converter_from_typeid: dict[
208
230
  }
209
231
 
210
232
 
211
- def _get_specifier(value: Value):
233
+ def _get_specifier(value: Value) -> str:
212
234
  typ = value.type("value").aspy()
213
235
  if isinstance(typ, tuple):
214
236
  return typ[0]
@@ -258,7 +280,7 @@ def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverte
258
280
  == 0
259
281
  ):
260
282
  # Allow int signals to represent float records when prec is 0
261
- return PvaConverter(int)
283
+ return PvaIntConverter()
262
284
  elif inferred_datatype is str and (enum_cls := get_enum_cls(datatype)):
263
285
  # Allow strings to be used as enums until QSRV supports this
264
286
  return PvaConverter(str)
@@ -301,7 +323,7 @@ async def pvget_with_timeout(pv: str, timeout: float) -> Any:
301
323
  try:
302
324
  return await asyncio.wait_for(context().get(pv), timeout=timeout)
303
325
  except asyncio.TimeoutError as exc:
304
- logging.debug(f"signal pva://{pv} timed out", exc_info=True)
326
+ logger.debug(f"signal pva://{pv} timed out", exc_info=True)
305
327
  raise NotConnected(f"pva://{pv}") from exc
306
328
 
307
329
 
@@ -383,10 +405,15 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
383
405
  return self.converter.value(value)
384
406
 
385
407
  def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
408
+ if callback and self.subscription:
409
+ msg = "Cannot set a callback when one is already set"
410
+ raise RuntimeError(msg)
411
+
412
+ if self.subscription:
413
+ self.subscription.close()
414
+ self.subscription = None
415
+
386
416
  if callback:
387
- assert (
388
- not self.subscription
389
- ), "Cannot set a callback when one is already set"
390
417
 
391
418
  async def async_callback(v):
392
419
  callback(self._make_reading(v))
@@ -397,6 +424,3 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
397
424
  self.subscription = context().monitor(
398
425
  self.read_pv, async_callback, request=request
399
426
  )
400
- elif self.subscription:
401
- self.subscription.close()
402
- self.subscription = None
@@ -47,7 +47,7 @@ def get_supported_values(
47
47
 
48
48
 
49
49
  def format_datatype(datatype: Any) -> str:
50
- if get_origin(datatype) is np.ndarray and get_args(datatype)[0] == tuple[int]:
50
+ if get_origin(datatype) is np.ndarray and get_args(datatype):
51
51
  dtype = get_dtype(datatype)
52
52
  return f"Array1D[np.{dtype.name}]"
53
53
  elif get_origin(datatype) is Sequence: