ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.0a1__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 (151) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +97 -62
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +106 -125
  8. ophyd_async/core/_device.py +69 -63
  9. ophyd_async/core/_device_filler.py +65 -1
  10. ophyd_async/core/_flyer.py +14 -5
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +44 -35
  17. ophyd_async/core/_settings.py +36 -27
  18. ophyd_async/core/_signal.py +262 -170
  19. ophyd_async/core/_signal_backend.py +56 -13
  20. ophyd_async/core/_soft_signal_backend.py +16 -11
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +37 -8
  23. ophyd_async/core/_utils.py +96 -49
  24. ophyd_async/core/_yaml_settings.py +2 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/_andor.py +2 -2
  27. ophyd_async/epics/adandor/_andor_controller.py +4 -2
  28. ophyd_async/epics/adandor/_andor_io.py +2 -4
  29. ophyd_async/epics/adaravis/__init__.py +5 -0
  30. ophyd_async/epics/adaravis/_aravis.py +4 -8
  31. ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
  32. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  33. ophyd_async/epics/adcore/__init__.py +23 -8
  34. ophyd_async/epics/adcore/_core_detector.py +42 -2
  35. ophyd_async/epics/adcore/_core_io.py +124 -99
  36. ophyd_async/epics/adcore/_core_logic.py +106 -27
  37. ophyd_async/epics/adcore/_core_writer.py +12 -8
  38. ophyd_async/epics/adcore/_hdf_writer.py +21 -38
  39. ophyd_async/epics/adcore/_single_trigger.py +2 -2
  40. ophyd_async/epics/adcore/_utils.py +2 -2
  41. ophyd_async/epics/adkinetix/__init__.py +2 -1
  42. ophyd_async/epics/adkinetix/_kinetix.py +3 -3
  43. ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
  44. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  45. ophyd_async/epics/adpilatus/__init__.py +5 -0
  46. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  47. ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
  48. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  49. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  50. ophyd_async/epics/adsimdetector/_sim.py +4 -14
  51. ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
  52. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  53. ophyd_async/epics/advimba/__init__.py +10 -1
  54. ophyd_async/epics/advimba/_vimba.py +3 -2
  55. ophyd_async/epics/advimba/_vimba_controller.py +4 -2
  56. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  57. ophyd_async/epics/core/_aioca.py +35 -16
  58. ophyd_async/epics/core/_epics_connector.py +4 -0
  59. ophyd_async/epics/core/_epics_device.py +2 -0
  60. ophyd_async/epics/core/_p4p.py +10 -2
  61. ophyd_async/epics/core/_pvi_connector.py +65 -8
  62. ophyd_async/epics/core/_signal.py +51 -51
  63. ophyd_async/epics/core/_util.py +4 -4
  64. ophyd_async/epics/demo/__init__.py +16 -0
  65. ophyd_async/epics/demo/__main__.py +31 -0
  66. ophyd_async/epics/demo/_ioc.py +32 -0
  67. ophyd_async/epics/demo/_motor.py +82 -0
  68. ophyd_async/epics/demo/_point_detector.py +42 -0
  69. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  70. ophyd_async/epics/demo/_stage.py +15 -0
  71. ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
  72. ophyd_async/epics/demo/point_detector.db +59 -0
  73. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  74. ophyd_async/epics/eiger/_eiger.py +1 -3
  75. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  76. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  77. ophyd_async/epics/eiger/_odin_io.py +1 -2
  78. ophyd_async/epics/motor.py +65 -28
  79. ophyd_async/epics/signal.py +4 -1
  80. ophyd_async/epics/testing/_example_ioc.py +21 -9
  81. ophyd_async/epics/testing/_utils.py +3 -0
  82. ophyd_async/epics/testing/test_records.db +8 -0
  83. ophyd_async/epics/testing/test_records_pva.db +17 -16
  84. ophyd_async/fastcs/__init__.py +1 -0
  85. ophyd_async/fastcs/core.py +6 -0
  86. ophyd_async/fastcs/odin/__init__.py +1 -0
  87. ophyd_async/fastcs/panda/__init__.py +8 -6
  88. ophyd_async/fastcs/panda/_block.py +29 -9
  89. ophyd_async/fastcs/panda/_control.py +5 -0
  90. ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
  91. ophyd_async/fastcs/panda/_table.py +9 -6
  92. ophyd_async/fastcs/panda/_trigger.py +23 -9
  93. ophyd_async/fastcs/panda/_writer.py +27 -30
  94. ophyd_async/plan_stubs/__init__.py +2 -0
  95. ophyd_async/plan_stubs/_ensure_connected.py +1 -0
  96. ophyd_async/plan_stubs/_fly.py +2 -4
  97. ophyd_async/plan_stubs/_nd_attributes.py +2 -0
  98. ophyd_async/plan_stubs/_panda.py +1 -0
  99. ophyd_async/plan_stubs/_settings.py +43 -16
  100. ophyd_async/plan_stubs/_utils.py +3 -0
  101. ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
  102. ophyd_async/sim/__init__.py +24 -14
  103. ophyd_async/sim/__main__.py +43 -0
  104. ophyd_async/sim/_blob_detector.py +33 -0
  105. ophyd_async/sim/_blob_detector_controller.py +48 -0
  106. ophyd_async/sim/_blob_detector_writer.py +105 -0
  107. ophyd_async/sim/_mirror_horizontal.py +46 -0
  108. ophyd_async/sim/_mirror_vertical.py +74 -0
  109. ophyd_async/sim/_motor.py +233 -0
  110. ophyd_async/sim/_pattern_generator.py +124 -0
  111. ophyd_async/sim/_point_detector.py +86 -0
  112. ophyd_async/sim/_stage.py +19 -0
  113. ophyd_async/tango/__init__.py +1 -0
  114. ophyd_async/tango/core/__init__.py +6 -1
  115. ophyd_async/tango/core/_base_device.py +41 -33
  116. ophyd_async/tango/core/_converters.py +81 -0
  117. ophyd_async/tango/core/_signal.py +18 -32
  118. ophyd_async/tango/core/_tango_readable.py +2 -19
  119. ophyd_async/tango/core/_tango_transport.py +136 -60
  120. ophyd_async/tango/core/_utils.py +47 -0
  121. ophyd_async/tango/{sim → demo}/_counter.py +2 -0
  122. ophyd_async/tango/{sim → demo}/_detector.py +2 -0
  123. ophyd_async/tango/{sim → demo}/_mover.py +5 -4
  124. ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
  125. ophyd_async/tango/testing/__init__.py +6 -0
  126. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  127. ophyd_async/testing/__init__.py +29 -7
  128. ophyd_async/testing/_assert.py +137 -81
  129. ophyd_async/testing/_mock_signal_utils.py +56 -70
  130. ophyd_async/testing/_one_of_everything.py +41 -21
  131. ophyd_async/testing/_single_derived.py +87 -0
  132. ophyd_async/testing/_utils.py +3 -0
  133. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
  134. ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
  135. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
  136. ophyd_async/epics/sim/__init__.py +0 -54
  137. ophyd_async/epics/sim/_ioc.py +0 -29
  138. ophyd_async/epics/sim/_mover.py +0 -101
  139. ophyd_async/epics/sim/_sensor.py +0 -37
  140. ophyd_async/epics/sim/sensor.db +0 -19
  141. ophyd_async/sim/_pattern_detector/__init__.py +0 -13
  142. ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
  143. ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
  144. ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
  145. ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
  146. ophyd_async/sim/_sim_motor.py +0 -107
  147. ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
  148. /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
  149. /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
  150. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
  151. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
@@ -1,33 +1,33 @@
1
1
  import asyncio
2
+ from typing import Annotated as A
2
3
 
3
- from ophyd_async.core import Device, StrictEnum
4
- from ophyd_async.core._providers import DatasetDescriber
5
- from ophyd_async.epics.core import (
6
- epics_signal_r,
7
- epics_signal_rw,
8
- epics_signal_rw_rbv,
9
- )
4
+ from ophyd_async.core import DatasetDescriber, SignalR, SignalRW, StrictEnum
5
+ from ophyd_async.epics.core import EpicsDevice, PvSuffix
10
6
 
11
- from ._utils import ADBaseDataType, FileWriteMode, ImageMode, convert_ad_dtype_to_np
7
+ from ._utils import ADBaseDataType, ADFileWriteMode, ADImageMode, convert_ad_dtype_to_np
12
8
 
13
9
 
14
- class Callback(StrictEnum):
10
+ class ADCallbacks(StrictEnum):
15
11
  ENABLE = "Enable"
16
12
  DISABLE = "Disable"
17
13
 
18
14
 
19
- class NDArrayBaseIO(Device):
20
- def __init__(self, prefix: str, name: str = "") -> None:
21
- self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV")
22
- self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile")
23
- self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire")
24
- self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
25
- self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
26
- self.data_type = epics_signal_r(ADBaseDataType, prefix + "DataType_RBV")
27
- self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter")
28
- # There is no _RBV for this one
29
- self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins")
30
- super().__init__(name=name)
15
+ class NDArrayBaseIO(EpicsDevice):
16
+ """Class responsible for passing detector data from drivers to pluglins.
17
+
18
+ This mirrors the interface provided by ADCore/db/NDArrayBase.template.
19
+ See HTML docs at https://areadetector.github.io/areaDetector/ADCore/NDArray.html
20
+ """
21
+
22
+ unique_id: A[SignalR[int], PvSuffix("UniqueId_RBV")]
23
+ nd_attributes_file: A[SignalRW[str], PvSuffix("NDAttributesFile")]
24
+ acquire: A[SignalRW[bool], PvSuffix.rbv("Acquire")]
25
+ array_size_x: A[SignalR[int], PvSuffix("ArraySizeX_RBV")]
26
+ array_size_y: A[SignalR[int], PvSuffix("ArraySizeY_RBV")]
27
+ data_type: A[SignalR[ADBaseDataType], PvSuffix("DataType_RBV")]
28
+ array_counter: A[SignalRW[int], PvSuffix.rbv("ArrayCounter")]
29
+ # There is no _RBV for this one
30
+ wait_for_plugins: A[SignalRW[bool], PvSuffix("WaitForPlugins")]
31
31
 
32
32
 
33
33
  class ADBaseDatasetDescriber(DatasetDescriber):
@@ -46,49 +46,51 @@ class ADBaseDatasetDescriber(DatasetDescriber):
46
46
 
47
47
 
48
48
  class NDPluginBaseIO(NDArrayBaseIO):
49
- def __init__(self, prefix: str, name: str = "") -> None:
50
- self.nd_array_port = epics_signal_rw_rbv(str, prefix + "NDArrayPort")
51
- self.enable_callbacks = epics_signal_rw_rbv(
52
- Callback, prefix + "EnableCallbacks"
53
- )
54
- self.nd_array_address = epics_signal_rw_rbv(int, prefix + "NDArrayAddress")
55
- self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV")
56
- self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV")
57
- self.queue_size = epics_signal_rw(int, prefix + "QueueSize")
58
- super().__init__(prefix, name)
49
+ """Base class from which plugins are derived.
50
+
51
+ This mirrors the interface provided by ADCore/db/NDPluginBase.template.
52
+ See HTML docs at https://areadetector.github.io/areaDetector/ADCore/NDPluginDriver.html
53
+ """
54
+
55
+ nd_array_port: A[SignalRW[str], PvSuffix.rbv("NDArrayPort")]
56
+ enable_callbacks: A[SignalRW[ADCallbacks], PvSuffix.rbv("EnableCallbacks")]
57
+ nd_array_address: A[SignalRW[int], PvSuffix.rbv("NDArrayAddress")]
58
+ array_size0: A[SignalR[int], PvSuffix("ArraySize0_RBV")]
59
+ array_size1: A[SignalR[int], PvSuffix("ArraySize1_RBV")]
60
+ queue_size: A[SignalRW[int], PvSuffix.rbv("QueueSize")]
59
61
 
60
62
 
61
63
  class NDPluginStatsIO(NDPluginBaseIO):
62
- """
63
- Plugin for computing statistics from an image or region of interest within an image.
64
- """
64
+ """Plugin for computing statistics from an image or ROI within an image.
65
65
 
66
- def __init__(self, prefix: str, name: str = "") -> None:
67
- # Basic statistics
68
- self.compute_statistics = epics_signal_rw(bool, prefix + "ComputeStatistics")
69
- self.bgd_width = epics_signal_rw(int, prefix + "BgdWidth")
70
- self.total_array = epics_signal_rw(float, prefix + "TotalArray")
71
- # Centroid statistics
72
- self.compute_centroid = epics_signal_rw(bool, prefix + "ComputeCentroid")
73
- self.centroid_threshold = epics_signal_rw(float, prefix + "CentroidThreshold")
74
- # X and Y Profiles
75
- self.compute_profiles = epics_signal_rw(bool, prefix + "ComputeProfiles")
76
- self.profile_size_x = epics_signal_rw(int, prefix + "ProfileSizeX")
77
- self.profile_size_y = epics_signal_rw(int, prefix + "ProfileSizeY")
78
- self.cursor_x = epics_signal_rw(int, prefix + "CursorX")
79
- self.cursor_y = epics_signal_rw(int, prefix + "CursorY")
80
- # Array Histogram
81
- self.compute_histogram = epics_signal_rw(bool, prefix + "ComputeHistogram")
82
- self.hist_size = epics_signal_rw(int, prefix + "HistSize")
83
- self.hist_min = epics_signal_rw(float, prefix + "HistMin")
84
- self.hist_max = epics_signal_rw(float, prefix + "HistMax")
85
- super().__init__(prefix, name)
86
-
87
-
88
- class DetectorState(StrictEnum):
66
+ This mirrors the interface provided by ADCore/db/NDStats.template.
67
+ See HTML docs at https://areadetector.github.io/areaDetector/ADCore/NDPluginStats.html
89
68
  """
90
- Default set of states of an AreaDetector driver.
91
- See definition in ADApp/ADSrc/ADDriver.h in https://github.com/areaDetector/ADCore
69
+
70
+ # Basic statistics
71
+ compute_statistics: A[SignalRW[bool], PvSuffix.rbv("ComputeStatistics")]
72
+ bgd_width: A[SignalRW[int], PvSuffix.rbv("BgdWidth")]
73
+ total_array: A[SignalRW[float], PvSuffix.rbv("TotalArray")]
74
+ # Centroid statistics
75
+ compute_centroid: A[SignalRW[bool], PvSuffix.rbv("ComputeCentroid")]
76
+ centroid_threshold: A[SignalRW[float], PvSuffix.rbv("CentroidThreshold")]
77
+ # X and Y Profiles
78
+ compute_profiles: A[SignalRW[bool], PvSuffix.rbv("ComputeProfiles")]
79
+ profile_size_x: A[SignalRW[int], PvSuffix.rbv("ProfileSizeX")]
80
+ profile_size_y: A[SignalRW[int], PvSuffix.rbv("ProfileSizeY")]
81
+ cursor_x: A[SignalRW[int], PvSuffix.rbv("CursorX")]
82
+ cursor_y: A[SignalRW[int], PvSuffix.rbv("CursorY")]
83
+ # Array Histogram
84
+ compute_histogram: A[SignalRW[bool], PvSuffix.rbv("ComputeHistogram")]
85
+ hist_size: A[SignalRW[int], PvSuffix.rbv("HistSize")]
86
+ hist_min: A[SignalRW[float], PvSuffix.rbv("HistMin")]
87
+ hist_max: A[SignalRW[float], PvSuffix.rbv("HistMax")]
88
+
89
+
90
+ class ADState(StrictEnum):
91
+ """Default set of states of an AreaDetector driver.
92
+
93
+ See definition in ADApp/ADSrc/ADDriver.h in https://github.com/areaDetector/ADCore.
92
94
  """
93
95
 
94
96
  IDLE = "Idle"
@@ -105,19 +107,20 @@ class DetectorState(StrictEnum):
105
107
 
106
108
 
107
109
  class ADBaseIO(NDArrayBaseIO):
108
- def __init__(self, prefix: str, name: str = "") -> None:
109
- # Define some signals
110
- self.acquire_time = epics_signal_rw_rbv(float, prefix + "AcquireTime")
111
- self.acquire_period = epics_signal_rw_rbv(float, prefix + "AcquirePeriod")
112
- self.num_images = epics_signal_rw_rbv(int, prefix + "NumImages")
113
- self.image_mode = epics_signal_rw_rbv(ImageMode, prefix + "ImageMode")
114
- self.detector_state = epics_signal_r(
115
- DetectorState, prefix + "DetectorState_RBV"
116
- )
117
- super().__init__(prefix, name=name)
110
+ """Base class from which areaDetector drivers are derived.
111
+
112
+ This mirrors the interface provided by ADCore/db/ADBase.template.
113
+ See HTML docs at https://areadetector.github.io/areaDetector/ADCore/ADDriver.html
114
+ """
115
+
116
+ acquire_time: A[SignalRW[float], PvSuffix.rbv("AcquireTime")]
117
+ acquire_period: A[SignalRW[float], PvSuffix.rbv("AcquirePeriod")]
118
+ num_images: A[SignalRW[int], PvSuffix.rbv("NumImages")]
119
+ image_mode: A[SignalRW[ADImageMode], PvSuffix.rbv("ImageMode")]
120
+ detector_state: A[SignalR[ADState], PvSuffix("DetectorState_RBV")]
118
121
 
119
122
 
120
- class Compression(StrictEnum):
123
+ class ADCompression(StrictEnum):
121
124
  NONE = "None"
122
125
  NBIT = "N-bit"
123
126
  SZIP = "szip"
@@ -129,35 +132,57 @@ class Compression(StrictEnum):
129
132
 
130
133
 
131
134
  class NDFileIO(NDPluginBaseIO):
132
- def __init__(self, prefix: str, name="") -> None:
133
- self.file_path = epics_signal_rw_rbv(str, prefix + "FilePath")
134
- self.file_name = epics_signal_rw_rbv(str, prefix + "FileName")
135
- self.file_path_exists = epics_signal_r(bool, prefix + "FilePathExists_RBV")
136
- self.file_template = epics_signal_rw_rbv(str, prefix + "FileTemplate")
137
- self.full_file_name = epics_signal_r(str, prefix + "FullFileName_RBV")
138
- self.file_number = epics_signal_rw(int, prefix + "FileNumber")
139
- self.auto_increment = epics_signal_rw(bool, prefix + "AutoIncrement")
140
- self.file_write_mode = epics_signal_rw_rbv(
141
- FileWriteMode, prefix + "FileWriteMode"
142
- )
143
- self.num_capture = epics_signal_rw_rbv(int, prefix + "NumCapture")
144
- self.num_captured = epics_signal_r(int, prefix + "NumCaptured_RBV")
145
- self.capture = epics_signal_rw_rbv(bool, prefix + "Capture")
146
- self.array_size0 = epics_signal_r(int, prefix + "ArraySize0")
147
- self.array_size1 = epics_signal_r(int, prefix + "ArraySize1")
148
- self.create_directory = epics_signal_rw(int, prefix + "CreateDirectory")
149
- super().__init__(prefix, name)
135
+ """Base class from which file plugins are derived.
136
+
137
+ This mirrors the interface provided by ADCore/db/NDFile.template.
138
+ See HTML docs at https://areadetector.github.io/areaDetector/ADCore/NDPluginFile.html
139
+ """
140
+
141
+ file_path: A[SignalRW[str], PvSuffix.rbv("FilePath")]
142
+ file_name: A[SignalRW[str], PvSuffix.rbv("FileName")]
143
+ file_path_exists: A[SignalR[bool], PvSuffix("FilePathExists_RBV")]
144
+ file_template: A[SignalRW[str], PvSuffix.rbv("FileTemplate")]
145
+ full_file_name: A[SignalR[str], PvSuffix("FullFileName_RBV")]
146
+ file_number: A[SignalRW[int], PvSuffix("FileNumber")]
147
+ auto_increment: A[SignalRW[bool], PvSuffix("AutoIncrement")]
148
+ file_write_mode: A[SignalRW[ADFileWriteMode], PvSuffix.rbv("FileWriteMode")]
149
+ num_capture: A[SignalRW[int], PvSuffix.rbv("NumCapture")]
150
+ num_captured: A[SignalR[int], PvSuffix("NumCaptured_RBV")]
151
+ capture: A[SignalRW[bool], PvSuffix.rbv("Capture")]
152
+ array_size0: A[SignalR[int], PvSuffix("ArraySize0")]
153
+ array_size1: A[SignalR[int], PvSuffix("ArraySize1")]
154
+ create_directory: A[SignalRW[int], PvSuffix("CreateDirectory")]
150
155
 
151
156
 
152
157
  class NDFileHDFIO(NDFileIO):
153
- def __init__(self, prefix: str, name="") -> None:
154
- self.position_mode = epics_signal_rw_rbv(bool, prefix + "PositionMode")
155
- self.compression = epics_signal_rw_rbv(Compression, prefix + "Compression")
156
- self.num_extra_dims = epics_signal_rw_rbv(int, prefix + "NumExtraDims")
157
- self.swmr_mode = epics_signal_rw_rbv(bool, prefix + "SWMRMode")
158
- self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
159
- self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName")
160
- self.num_frames_chunks = epics_signal_r(int, prefix + "NumFramesChunks_RBV")
161
- self.chunk_size_auto = epics_signal_rw_rbv(bool, prefix + "ChunkSizeAuto")
162
- self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen")
163
- super().__init__(prefix, name)
158
+ """Plugin for storing data in HDF5 file format.
159
+
160
+ This mirrors the interface provided by ADCore/db/NDFileHDF5.template.
161
+ See HTML docs at https://areadetector.github.io/areaDetector/ADCore/NDFileHDF5.html
162
+ """
163
+
164
+ position_mode: A[SignalRW[bool], PvSuffix.rbv("PositionMode")]
165
+ compression: A[SignalRW[ADCompression], PvSuffix.rbv("Compression")]
166
+ num_extra_dims: A[SignalRW[int], PvSuffix.rbv("NumExtraDims")]
167
+ swmr_mode: A[SignalRW[bool], PvSuffix.rbv("SWMRMode")]
168
+ flush_now: A[SignalRW[bool], PvSuffix("FlushNow")]
169
+ xml_file_name: A[SignalRW[str], PvSuffix.rbv("XMLFileName")]
170
+ num_frames_chunks: A[SignalR[int], PvSuffix("NumFramesChunks_RBV")]
171
+ chunk_size_auto: A[SignalRW[bool], PvSuffix.rbv("ChunkSizeAuto")]
172
+ lazy_open: A[SignalRW[bool], PvSuffix.rbv("LazyOpen")]
173
+
174
+
175
+ class NDCBFlushOnSoftTrgMode(StrictEnum):
176
+ ON_NEW_IMAGE = "OnNewImage"
177
+ IMMEDIATELY = "Immediately"
178
+
179
+
180
+ class NDPluginCBIO(NDPluginBaseIO):
181
+ pre_count: A[SignalRW[int], PvSuffix.rbv("PreCount")]
182
+ post_count: A[SignalRW[int], PvSuffix.rbv("PostCount")]
183
+ preset_trigger_count: A[SignalRW[int], PvSuffix.rbv("PresetTriggerCount")]
184
+ trigger: A[SignalRW[bool], PvSuffix.rbv("Trigger")]
185
+ capture: A[SignalRW[bool], PvSuffix.rbv("Capture")]
186
+ flush_on_soft_trg: A[
187
+ SignalRW[NDCBFlushOnSoftTrgMode], PvSuffix.rbv("FlushOnSoftTrg")
188
+ ]
@@ -10,14 +10,18 @@ from ophyd_async.core import (
10
10
  set_and_wait_for_value,
11
11
  )
12
12
 
13
- from ._core_io import ADBaseIO, DetectorState
14
- from ._utils import ImageMode, stop_busy_record
13
+ from ._core_io import (
14
+ ADBaseIO,
15
+ ADCallbacks,
16
+ ADState,
17
+ NDCBFlushOnSoftTrgMode,
18
+ NDPluginCBIO,
19
+ )
20
+ from ._utils import ADImageMode, stop_busy_record
15
21
 
16
22
  # Default set of states that we should consider "good" i.e. the acquisition
17
23
  # is complete and went well
18
- DEFAULT_GOOD_STATES: frozenset[DetectorState] = frozenset(
19
- [DetectorState.IDLE, DetectorState.ABORTED]
20
- )
24
+ DEFAULT_GOOD_STATES: frozenset[ADState] = frozenset([ADState.IDLE, ADState.ABORTED])
21
25
 
22
26
  ADBaseIOT = TypeVar("ADBaseIOT", bound=ADBaseIO)
23
27
  ADBaseControllerT = TypeVar("ADBaseControllerT", bound="ADBaseController")
@@ -27,7 +31,7 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
27
31
  def __init__(
28
32
  self,
29
33
  driver: ADBaseIOT,
30
- good_states: frozenset[DetectorState] = DEFAULT_GOOD_STATES,
34
+ good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
31
35
  ) -> None:
32
36
  self.driver = driver
33
37
  self.good_states = good_states
@@ -46,15 +50,16 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
46
50
  )
47
51
  await asyncio.gather(
48
52
  self.driver.num_images.set(trigger_info.total_number_of_triggers),
49
- self.driver.image_mode.set(ImageMode.MULTIPLE),
53
+ self.driver.image_mode.set(ADImageMode.MULTIPLE),
50
54
  )
51
55
 
52
56
  async def arm(self):
53
57
  self._arm_status = await self.start_acquiring_driver_and_ensure_status()
54
58
 
55
59
  async def wait_for_idle(self):
56
- if self._arm_status:
60
+ if self._arm_status and not self._arm_status.done:
57
61
  await self._arm_status
62
+ self._arm_status = None
58
63
 
59
64
  async def disarm(self):
60
65
  # We can't use caput callback as we already used it in arm() and we can't have
@@ -66,17 +71,16 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
66
71
  exposure: float | None = None,
67
72
  timeout: float = DEFAULT_TIMEOUT,
68
73
  ) -> None:
69
- """
70
- Sets the exposure time if it is not None and the acquire period to the
71
- exposure time plus the deadtime. This is expected behavior for most
72
- AreaDetectors, but some may require more specialized handling.
73
-
74
- Parameters
75
- ----------
76
- exposure:
77
- Desired exposure time, this is a noop if it is None.
78
- timeout:
79
- How long to wait for the exposure time and acquire period to be set.
74
+ """Set the exposure time and acquire period.
75
+
76
+ If exposure is not None, this sets the acquire time to the exposure time
77
+ and sets the acquire period to the exposure time plus the deadtime. This
78
+ is expected behavior for most AreaDetectors, but some may require more
79
+ specialized handling.
80
+
81
+ :param exposure: Desired exposure time, this is a noop if it is None.
82
+ :type exposure: How long to wait for the exposure time and acquire
83
+ period to be set.
80
84
  """
81
85
  if exposure is not None:
82
86
  full_frame_time = exposure + self.get_deadtime(exposure)
@@ -86,20 +90,16 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
86
90
  )
87
91
 
88
92
  async def start_acquiring_driver_and_ensure_status(self) -> AsyncStatus:
89
- """
90
- Start acquiring driver, raising ValueError if the detector is in a bad state.
93
+ """Start acquiring driver, raising ValueError if the detector is in a bad state.
91
94
 
92
95
  This sets driver.acquire to True, and waits for it to be True up to a timeout.
93
96
  Then, it checks that the DetectorState PV is in DEFAULT_GOOD_STATES,
94
97
  and otherwise raises a ValueError.
95
98
 
96
- Returns
97
- -------
98
- AsyncStatus:
99
+ :returns AsyncStatus:
99
100
  An AsyncStatus that can be awaited to set driver.acquire to True and perform
100
101
  subsequent raising (if applicable) due to detector state.
101
102
  """
102
-
103
103
  status = await set_and_wait_for_value(
104
104
  self.driver.acquire,
105
105
  True,
@@ -108,8 +108,8 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
108
108
  )
109
109
 
110
110
  async def complete_acquisition() -> None:
111
- """NOTE: possible race condition here between the callback from
112
- set_and_wait_for_value and the detector state updating."""
111
+ # NOTE: possible race condition here between the callback from
112
+ # set_and_wait_for_value and the detector state updating.
113
113
  await status
114
114
  state = await self.driver.detector_state.get_value()
115
115
  if state not in self.good_states:
@@ -119,3 +119,82 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
119
119
  )
120
120
 
121
121
  return AsyncStatus(complete_acquisition())
122
+
123
+
124
+ class ADBaseContAcqController(ADBaseController[ADBaseIO]):
125
+ """Continuous acquisition interface for an AreaDetector."""
126
+
127
+ def __init__(self, driver: ADBaseIO, cb_plugin: NDPluginCBIO) -> None:
128
+ self.cb_plugin = cb_plugin
129
+ super().__init__(driver)
130
+
131
+ def get_deadtime(self, exposure):
132
+ # For now just set this to something until we can decide how to pass this in
133
+ return 0.001
134
+
135
+ async def ensure_acquisition_settings_valid(
136
+ self, trigger_info: TriggerInfo
137
+ ) -> None:
138
+ """Ensure the trigger mode is valid for the detector."""
139
+ if trigger_info.trigger != DetectorTrigger.INTERNAL:
140
+ # Note that not all detectors will use the `DetectorTrigger` enum
141
+ raise TypeError(
142
+ "The continuous acq interface only supports internal triggering."
143
+ )
144
+
145
+ # Not all detectors allow for changing exposure times during an acquisition,
146
+ # so in this least-common-denominator implementation check to see if
147
+ # exposure time matches the current exposure time.
148
+ exposure_time = await self.driver.acquire_time.get_value()
149
+ if trigger_info.livetime is not None and trigger_info.livetime != exposure_time:
150
+ raise ValueError(
151
+ f"Detector exposure time currently set to {exposure_time}, "
152
+ f"but requested exposure is {trigger_info.livetime}"
153
+ )
154
+
155
+ async def ensure_in_continuous_acquisition_mode(self) -> None:
156
+ """Ensure the detector is in continuous acquisition mode."""
157
+ image_mode = await self.driver.image_mode.get_value()
158
+ acquiring = await self.driver.acquire.get_value()
159
+
160
+ if image_mode != ADImageMode.CONTINUOUS or not acquiring:
161
+ raise RuntimeError(
162
+ "Driver must be acquiring in continuous mode to use the "
163
+ "cont acq interface"
164
+ )
165
+
166
+ async def prepare(self, trigger_info: TriggerInfo) -> None:
167
+ # These are broken out into seperate functions to make it easier to
168
+ # override them in subclasses, for example if you want `prepare` to
169
+ # setup the detector in continuous mode if it isn't already (for now,
170
+ # we assume it already is). If your detector uses different enums
171
+ # for `ImageMode` or `DetectorTrigger`, you should also override these.
172
+ await self.ensure_acquisition_settings_valid(trigger_info)
173
+ await self.ensure_in_continuous_acquisition_mode()
174
+
175
+ # Configure the CB plugin to collect the correct number of triggers
176
+ await asyncio.gather(
177
+ self.cb_plugin.enable_callbacks.set(ADCallbacks.ENABLE),
178
+ self.cb_plugin.pre_count.set(0),
179
+ self.cb_plugin.post_count.set(trigger_info.total_number_of_triggers),
180
+ self.cb_plugin.preset_trigger_count.set(1),
181
+ self.cb_plugin.flush_on_soft_trg.set(NDCBFlushOnSoftTrgMode.ON_NEW_IMAGE),
182
+ )
183
+
184
+ async def arm(self) -> None:
185
+ # Start the CB plugin's capture, and cache it in the arm status attr
186
+ self._arm_status = await set_and_wait_for_value(
187
+ self.cb_plugin.capture,
188
+ True,
189
+ timeout=DEFAULT_TIMEOUT,
190
+ wait_for_set_completion=False,
191
+ )
192
+
193
+ # Send the trigger to begin acquisition
194
+ await self.cb_plugin.trigger.set(True, wait=False)
195
+
196
+ async def disarm(self) -> None:
197
+ await stop_busy_record(self.cb_plugin.capture, False, timeout=1)
198
+ if self._arm_status and not self._arm_status.done:
199
+ await self._arm_status
200
+ self._arm_status = None
@@ -24,18 +24,20 @@ from ophyd_async.core._utils import DEFAULT_TIMEOUT
24
24
  # from ophyd_async.epics.adcore._core_logic import ADBaseDatasetDescriber
25
25
  from ._core_io import (
26
26
  ADBaseDatasetDescriber,
27
- Callback,
27
+ ADCallbacks,
28
28
  NDArrayBaseIO,
29
29
  NDFileIO,
30
30
  NDPluginBaseIO,
31
31
  )
32
- from ._utils import FileWriteMode
32
+ from ._utils import ADFileWriteMode
33
33
 
34
34
  NDFileIOT = TypeVar("NDFileIOT", bound=NDFileIO)
35
35
  ADWriterT = TypeVar("ADWriterT", bound="ADWriter")
36
36
 
37
37
 
38
38
  class ADWriter(DetectorWriter, Generic[NDFileIOT]):
39
+ """Common behavior for all areaDetector writers."""
40
+
39
41
  default_suffix: str = "FILE1:"
40
42
 
41
43
  def __init__(
@@ -92,7 +94,7 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
92
94
  async def begin_capture(self) -> None:
93
95
  info = self._path_provider(device_name=self._name_provider())
94
96
 
95
- await self.fileio.enable_callbacks.set(Callback.ENABLE)
97
+ await self.fileio.enable_callbacks.set(ADCallbacks.ENABLE)
96
98
 
97
99
  # Set the directory creation depth first, since dir creation callback happens
98
100
  # when directory path PV is processed.
@@ -102,7 +104,7 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
102
104
  # See https://github.com/bluesky/ophyd-async/issues/122
103
105
  self.fileio.file_path.set(str(info.directory_path)),
104
106
  self.fileio.file_name.set(info.filename),
105
- self.fileio.file_write_mode.set(FileWriteMode.STREAM),
107
+ self.fileio.file_write_mode.set(ADFileWriteMode.STREAM),
106
108
  # For non-HDF file writers, use AD file templating mechanism
107
109
  # for generating multi-image datasets
108
110
  self.fileio.file_template.set(
@@ -134,7 +136,7 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
134
136
 
135
137
  describe = {
136
138
  self._name_provider(): DataKey(
137
- source=self._name_provider(),
139
+ source=self.fileio.full_file_name.source,
138
140
  shape=list(frame_shape),
139
141
  dtype="array",
140
142
  dtype_numpy=dtype_numpy,
@@ -144,9 +146,9 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
144
146
  return describe
145
147
 
146
148
  async def observe_indices_written(
147
- self, timeout=DEFAULT_TIMEOUT
149
+ self, timeout: float
148
150
  ) -> AsyncGenerator[int, None]:
149
- """Wait until a specific index is ready to be collected"""
151
+ """Wait until a specific index is ready to be collected."""
150
152
  async for num_captured in observe_value(self.fileio.num_captured, timeout):
151
153
  yield num_captured // self._multiplier
152
154
 
@@ -187,6 +189,7 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
187
189
  "chunk_shape": (1, *frame_shape),
188
190
  # Include file template for reconstruction in consolidator
189
191
  "template": file_template,
192
+ "multiplier": self._multiplier,
190
193
  },
191
194
  uid=None,
192
195
  validate=True,
@@ -210,9 +213,10 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
210
213
  # Already done a caput callback in _capture_status, so can't do one here
211
214
  await self.fileio.capture.set(False, wait=False)
212
215
  await wait_for_value(self.fileio.capture, False, DEFAULT_TIMEOUT)
213
- if self._capture_status:
216
+ if self._capture_status and not self._capture_status.done:
214
217
  # We kicked off an open, so wait for it to return
215
218
  await self._capture_status
219
+ self._capture_status = None
216
220
 
217
221
  @property
218
222
  def hints(self) -> Hints: