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.
- ophyd_async/__init__.py +1 -4
- ophyd_async/_version.py +1 -1
- ophyd_async/core/__init__.py +12 -2
- ophyd_async/core/_providers.py +3 -1
- ophyd_async/core/detector.py +65 -38
- ophyd_async/core/device.py +8 -0
- ophyd_async/core/flyer.py +10 -19
- ophyd_async/core/signal.py +36 -17
- ophyd_async/core/signal_backend.py +5 -2
- ophyd_async/core/sim_signal_backend.py +28 -16
- ophyd_async/core/standard_readable.py +4 -2
- ophyd_async/core/utils.py +18 -1
- ophyd_async/epics/_backend/_aioca.py +13 -11
- ophyd_async/epics/_backend/_p4p.py +19 -16
- ophyd_async/epics/_backend/common.py +16 -11
- ophyd_async/epics/areadetector/__init__.py +4 -0
- ophyd_async/epics/areadetector/aravis.py +69 -0
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +73 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +36 -24
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +154 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +4 -4
- ophyd_async/epics/areadetector/pilatus.py +50 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +4 -4
- ophyd_async/epics/areadetector/writers/hdf_writer.py +6 -1
- ophyd_async/epics/demo/__init__.py +33 -3
- ophyd_async/epics/motion/motor.py +20 -14
- ophyd_async/epics/pvi/__init__.py +3 -0
- ophyd_async/epics/pvi/pvi.py +318 -0
- ophyd_async/epics/signal/signal.py +26 -9
- ophyd_async/panda/__init__.py +17 -6
- ophyd_async/panda/_common_blocks.py +49 -0
- ophyd_async/panda/_hdf_panda.py +48 -0
- ophyd_async/panda/{panda_controller.py → _panda_controller.py} +3 -7
- ophyd_async/panda/_trigger.py +39 -0
- ophyd_async/panda/writers/__init__.py +3 -0
- ophyd_async/panda/writers/_hdf_writer.py +220 -0
- ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
- ophyd_async/planstubs/__init__.py +5 -0
- ophyd_async/planstubs/prepare_trigger_and_dets.py +57 -0
- ophyd_async/protocols.py +73 -0
- ophyd_async/sim/__init__.py +11 -0
- ophyd_async/sim/demo/__init__.py +3 -0
- ophyd_async/sim/demo/sim_motor.py +116 -0
- ophyd_async/sim/pattern_generator.py +318 -0
- ophyd_async/sim/sim_pattern_detector_control.py +55 -0
- ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
- ophyd_async/sim/sim_pattern_generator.py +37 -0
- {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/METADATA +19 -75
- ophyd_async-0.3a2.dist-info/RECORD +76 -0
- ophyd_async/epics/pvi.py +0 -70
- ophyd_async/panda/panda.py +0 -241
- ophyd_async-0.3a1.dist-info/RECORD +0 -56
- /ophyd_async/panda/{table.py → _table.py} +0 -0
- /ophyd_async/panda/{utils.py → _utils.py} +0 -0
- {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/WHEEL +0 -0
- {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/entry_points.txt +0 -0
- {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
|
|
56
|
-
value
|
|
57
|
-
timestamp
|
|
58
|
-
alarm_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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
53
|
-
value
|
|
54
|
-
timestamp
|
|
55
|
-
alarm_severity
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
2
|
+
from typing import Optional
|
|
3
3
|
|
|
4
|
-
from ophyd_async.core import AsyncStatus
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
28
|
-
self.good_states = good_states
|
|
27
|
+
self._drv = driver
|
|
29
28
|
|
|
30
29
|
def get_deadtime(self, exposure: float) -> float:
|
|
31
|
-
|
|
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.
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
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.
|
|
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
|
|
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(
|
|
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)
|