ophyd-async 0.8.0a5__py3-none-any.whl → 0.9.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 (64) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +4 -26
  3. ophyd_async/core/_detector.py +9 -9
  4. ophyd_async/core/_device.py +27 -8
  5. ophyd_async/core/_protocol.py +0 -28
  6. ophyd_async/core/_signal.py +111 -136
  7. ophyd_async/core/_table.py +9 -4
  8. ophyd_async/core/_utils.py +11 -2
  9. ophyd_async/epics/adaravis/_aravis_controller.py +8 -8
  10. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  11. ophyd_async/epics/adcore/_core_io.py +21 -21
  12. ophyd_async/epics/adcore/_core_logic.py +6 -3
  13. ophyd_async/epics/adcore/_hdf_writer.py +6 -3
  14. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  15. ophyd_async/epics/adcore/_utils.py +35 -35
  16. ophyd_async/epics/adkinetix/_kinetix_controller.py +7 -7
  17. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  18. ophyd_async/epics/adpilatus/_pilatus.py +3 -3
  19. ophyd_async/epics/adpilatus/_pilatus_controller.py +4 -4
  20. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  21. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  22. ophyd_async/epics/advimba/_vimba_controller.py +14 -14
  23. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  24. ophyd_async/epics/core/_p4p.py +19 -0
  25. ophyd_async/epics/core/_pvi_connector.py +4 -2
  26. ophyd_async/epics/core/_signal.py +9 -2
  27. ophyd_async/epics/core/_util.py +9 -0
  28. ophyd_async/epics/demo/_mover.py +2 -2
  29. ophyd_async/epics/demo/_sensor.py +2 -2
  30. ophyd_async/epics/eiger/_eiger_controller.py +10 -5
  31. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  32. ophyd_async/epics/motor.py +8 -5
  33. ophyd_async/epics/testing/__init__.py +24 -0
  34. ophyd_async/epics/testing/_example_ioc.py +107 -0
  35. ophyd_async/epics/testing/_utils.py +78 -0
  36. ophyd_async/epics/testing/test_records.db +158 -0
  37. ophyd_async/epics/testing/test_records_pva.db +177 -0
  38. ophyd_async/fastcs/core.py +2 -2
  39. ophyd_async/fastcs/panda/_block.py +9 -9
  40. ophyd_async/fastcs/panda/_control.py +2 -2
  41. ophyd_async/fastcs/panda/_hdf_panda.py +4 -1
  42. ophyd_async/fastcs/panda/_trigger.py +7 -7
  43. ophyd_async/plan_stubs/_fly.py +1 -1
  44. ophyd_async/sim/demo/_sim_motor.py +34 -32
  45. ophyd_async/tango/__init__.py +0 -43
  46. ophyd_async/tango/{signal → core}/__init__.py +7 -2
  47. ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
  48. ophyd_async/tango/{signal → core}/_signal.py +13 -3
  49. ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
  50. ophyd_async/tango/{signal → core}/_tango_transport.py +1 -1
  51. ophyd_async/tango/demo/_counter.py +6 -7
  52. ophyd_async/tango/demo/_mover.py +8 -7
  53. ophyd_async/testing/__init__.py +33 -0
  54. ophyd_async/testing/_assert.py +128 -0
  55. ophyd_async/{core → testing}/_mock_signal_utils.py +12 -8
  56. ophyd_async/testing/_wait_for_pending.py +22 -0
  57. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/METADATA +49 -47
  58. ophyd_async-0.9.0a1.dist-info/RECORD +119 -0
  59. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/WHEEL +1 -1
  60. ophyd_async/tango/base_devices/__init__.py +0 -4
  61. ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
  62. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/LICENSE +0 -0
  63. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/entry_points.txt +0 -0
  64. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Sequence
3
+ from collections.abc import Callable, Sequence
4
4
  from typing import Annotated, Any, TypeVar, get_origin
5
5
 
6
6
  import numpy as np
@@ -19,6 +19,13 @@ def _concat(value1, value2):
19
19
  return value1 + value2
20
20
 
21
21
 
22
+ def _make_default_factory(dtype: np.dtype) -> Callable[[], np.ndarray]:
23
+ def numpy_array_default_factory() -> np.ndarray:
24
+ return np.array([], dtype)
25
+
26
+ return numpy_array_default_factory
27
+
28
+
22
29
  class Table(BaseModel):
23
30
  """An abstraction of a Table of str to numpy array."""
24
31
 
@@ -45,9 +52,7 @@ class Table(BaseModel):
45
52
  NpArrayPydanticAnnotation.factory(
46
53
  data_type=dtype.type, dimensions=1, strict_data_typing=False
47
54
  ),
48
- Field(
49
- default_factory=lambda dtype=dtype: np.array([], dtype=dtype)
50
- ),
55
+ Field(default_factory=_make_default_factory(dtype)),
51
56
  ]
52
57
  elif get_origin(anno) is Sequence:
53
58
  new_anno = Annotated[anno, Field(default_factory=list)]
@@ -17,11 +17,20 @@ DEFAULT_TIMEOUT = 10.0
17
17
  ErrorText = str | Mapping[str, Exception]
18
18
 
19
19
 
20
- class StrictEnum(str, Enum):
20
+ class StrictEnumMeta(EnumMeta):
21
+ def __new__(metacls, *args, **kwargs):
22
+ ret = super().__new__(metacls, *args, **kwargs)
23
+ lowercase_names = [x.name for x in ret if not x.name.isupper()] # type: ignore
24
+ if lowercase_names:
25
+ raise TypeError(f"Names {lowercase_names} should be uppercase")
26
+ return ret
27
+
28
+
29
+ class StrictEnum(str, Enum, metaclass=StrictEnumMeta):
21
30
  """All members should exist in the Backend, and there will be no extras"""
22
31
 
23
32
 
24
- class SubsetEnumMeta(EnumMeta):
33
+ class SubsetEnumMeta(StrictEnumMeta):
25
34
  def __call__(self, value, *args, **kwargs): # type: ignore
26
35
  if isinstance(value, str) and not isinstance(value, self):
27
36
  return value
@@ -31,9 +31,9 @@ class AravisController(DetectorController):
31
31
 
32
32
  async def prepare(self, trigger_info: TriggerInfo):
33
33
  if trigger_info.total_number_of_triggers == 0:
34
- image_mode = adcore.ImageMode.continuous
34
+ image_mode = adcore.ImageMode.CONTINUOUS
35
35
  else:
36
- image_mode = adcore.ImageMode.multiple
36
+ image_mode = adcore.ImageMode.MULTIPLE
37
37
  if (exposure := trigger_info.livetime) is not None:
38
38
  await self._drv.acquire_time.set(exposure)
39
39
 
@@ -58,9 +58,9 @@ class AravisController(DetectorController):
58
58
  self, trigger: DetectorTrigger
59
59
  ) -> tuple[AravisTriggerMode, AravisTriggerSource]:
60
60
  supported_trigger_types = (
61
- DetectorTrigger.constant_gate,
62
- DetectorTrigger.edge_trigger,
63
- DetectorTrigger.internal,
61
+ DetectorTrigger.CONSTANT_GATE,
62
+ DetectorTrigger.EDGE_TRIGGER,
63
+ DetectorTrigger.INTERNAL,
64
64
  )
65
65
  if trigger not in supported_trigger_types:
66
66
  raise ValueError(
@@ -68,10 +68,10 @@ class AravisController(DetectorController):
68
68
  f"types: {supported_trigger_types} but was asked to "
69
69
  f"use {trigger}"
70
70
  )
71
- if trigger == DetectorTrigger.internal:
72
- return AravisTriggerMode.off, AravisTriggerSource.freerun
71
+ if trigger == DetectorTrigger.INTERNAL:
72
+ return AravisTriggerMode.OFF, AravisTriggerSource.FREERUN
73
73
  else:
74
- return (AravisTriggerMode.on, f"Line{self.gpio_number}") # type: ignore
74
+ return (AravisTriggerMode.ON, f"Line{self.gpio_number}") # type: ignore
75
75
 
76
76
  async def disarm(self):
77
77
  await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
@@ -6,8 +6,8 @@ from ophyd_async.epics.core import epics_signal_rw_rbv
6
6
  class AravisTriggerMode(StrictEnum):
7
7
  """GigEVision GenICAM standard: on=externally triggered"""
8
8
 
9
- on = "On"
10
- off = "Off"
9
+ ON = "On"
10
+ OFF = "Off"
11
11
 
12
12
 
13
13
  """A minimal set of TriggerSources that must be supported by the underlying record.
@@ -20,8 +20,8 @@ class AravisTriggerMode(StrictEnum):
20
20
 
21
21
 
22
22
  class AravisTriggerSource(SubsetEnum):
23
- freerun = "Freerun"
24
- line1 = "Line1"
23
+ FREERUN = "Freerun"
24
+ LINE1 = "Line1"
25
25
 
26
26
 
27
27
  class AravisDriverIO(adcore.ADBaseIO):
@@ -9,8 +9,8 @@ from ._utils import ADBaseDataType, FileWriteMode, ImageMode
9
9
 
10
10
 
11
11
  class Callback(StrictEnum):
12
- Enable = "Enable"
13
- Disable = "Disable"
12
+ ENABLE = "Enable"
13
+ DISABLE = "Disable"
14
14
 
15
15
 
16
16
  class NDArrayBaseIO(Device):
@@ -72,17 +72,17 @@ class DetectorState(StrictEnum):
72
72
  See definition in ADApp/ADSrc/ADDriver.h in https://github.com/areaDetector/ADCore
73
73
  """
74
74
 
75
- Idle = "Idle"
76
- Acquire = "Acquire"
77
- Readout = "Readout"
78
- Correct = "Correct"
79
- Saving = "Saving"
80
- Aborting = "Aborting"
81
- Error = "Error"
82
- Waiting = "Waiting"
83
- Initializing = "Initializing"
84
- Disconnected = "Disconnected"
85
- Aborted = "Aborted"
75
+ IDLE = "Idle"
76
+ ACQUIRE = "Acquire"
77
+ READOUT = "Readout"
78
+ CORRECT = "Correct"
79
+ SAVING = "Saving"
80
+ ABORTING = "Aborting"
81
+ ERROR = "Error"
82
+ WAITING = "Waiting"
83
+ INITIALIZING = "Initializing"
84
+ DISCONNECTED = "Disconnected"
85
+ ABORTED = "Aborted"
86
86
 
87
87
 
88
88
  class ADBaseIO(NDArrayBaseIO):
@@ -99,14 +99,14 @@ class ADBaseIO(NDArrayBaseIO):
99
99
 
100
100
 
101
101
  class Compression(StrictEnum):
102
- none = "None"
103
- nbit = "N-bit"
104
- szip = "szip"
105
- zlib = "zlib"
106
- blosc = "Blosc"
107
- bslz4 = "BSLZ4"
108
- lz4 = "LZ4"
109
- jpeg = "JPEG"
102
+ NONE = "None"
103
+ NBIT = "N-bit"
104
+ SZIP = "szip"
105
+ ZLIB = "zlib"
106
+ BLOSC = "Blosc"
107
+ BSLZ4 = "BSLZ4"
108
+ LZ4 = "LZ4"
109
+ JPEG = "JPEG"
110
110
 
111
111
 
112
112
  class NDFileHDFIO(NDPluginBaseIO):
@@ -14,7 +14,7 @@ from ._core_io import ADBaseIO, DetectorState
14
14
  # Default set of states that we should consider "good" i.e. the acquisition
15
15
  # is complete and went well
16
16
  DEFAULT_GOOD_STATES: frozenset[DetectorState] = frozenset(
17
- [DetectorState.Idle, DetectorState.Aborted]
17
+ [DetectorState.IDLE, DetectorState.ABORTED]
18
18
  )
19
19
 
20
20
 
@@ -91,7 +91,9 @@ async def start_acquiring_driver_and_ensure_status(
91
91
  subsequent raising (if applicable) due to detector state.
92
92
  """
93
93
 
94
- status = await set_and_wait_for_value(driver.acquire, True, timeout=timeout)
94
+ status = await set_and_wait_for_value(
95
+ driver.acquire, True, timeout=timeout, wait_for_set_completion=False
96
+ )
95
97
 
96
98
  async def complete_acquisition() -> None:
97
99
  """NOTE: possible race condition here between the callback from
@@ -100,7 +102,8 @@ async def start_acquiring_driver_and_ensure_status(
100
102
  state = await driver.detector_state.get_value()
101
103
  if state not in good_states:
102
104
  raise ValueError(
103
- f"Final detector state {state} not in valid end states: {good_states}"
105
+ f"Final detector state {state.value} not in valid end "
106
+ f"states: {good_states}"
104
107
  )
105
108
 
106
109
  return AsyncStatus(complete_acquisition())
@@ -20,7 +20,7 @@ from ophyd_async.core import (
20
20
  wait_for_value,
21
21
  )
22
22
 
23
- from ._core_io import NDArrayBaseIO, NDFileHDFIO
23
+ from ._core_io import Callback, NDArrayBaseIO, NDFileHDFIO
24
24
  from ._utils import (
25
25
  FileWriteMode,
26
26
  convert_param_dtype_to_np,
@@ -67,10 +67,11 @@ class ADHDFWriter(DetectorWriter):
67
67
  self.hdf.file_path.set(str(info.directory_path)),
68
68
  self.hdf.file_name.set(info.filename),
69
69
  self.hdf.file_template.set("%s/%s.h5"),
70
- self.hdf.file_write_mode.set(FileWriteMode.stream),
70
+ self.hdf.file_write_mode.set(FileWriteMode.STREAM),
71
71
  # Never use custom xml layout file but use the one defined
72
72
  # in the source code file NDFileHDF5LayoutXML.cpp
73
73
  self.hdf.xml_file_name.set(""),
74
+ self.hdf.enable_callbacks.set(Callback.ENABLE),
74
75
  )
75
76
 
76
77
  assert (
@@ -80,7 +81,9 @@ class ADHDFWriter(DetectorWriter):
80
81
  # Overwrite num_capture to go forever
81
82
  await self.hdf.num_capture.set(0)
82
83
  # Wait for it to start, stashing the status that tells us when it finishes
83
- self._capture_status = await set_and_wait_for_value(self.hdf.capture, True)
84
+ self._capture_status = await set_and_wait_for_value(
85
+ self.hdf.capture, True, wait_for_set_completion=False
86
+ )
84
87
  name = self._name_provider()
85
88
  detector_shape = await self._dataset_describer.shape()
86
89
  np_dtype = await self._dataset_describer.np_datatype()
@@ -34,7 +34,7 @@ class SingleTriggerDetector(StandardReadable, Triggerable):
34
34
  @AsyncStatus.wrap
35
35
  async def stage(self) -> None:
36
36
  await asyncio.gather(
37
- self.drv.image_mode.set(ImageMode.single),
37
+ self.drv.image_mode.set(ImageMode.SINGLE),
38
38
  self.drv.wait_for_plugins.set(True),
39
39
  )
40
40
  await super().stage()
@@ -11,42 +11,42 @@ from ophyd_async.core import (
11
11
 
12
12
 
13
13
  class ADBaseDataType(StrictEnum):
14
- Int8 = "Int8"
15
- UInt8 = "UInt8"
16
- Int16 = "Int16"
17
- UInt16 = "UInt16"
18
- Int32 = "Int32"
19
- UInt32 = "UInt32"
20
- Int64 = "Int64"
21
- UInt64 = "UInt64"
22
- Float32 = "Float32"
23
- Float64 = "Float64"
14
+ INT8 = "Int8"
15
+ UINT8 = "UInt8"
16
+ INT16 = "Int16"
17
+ UINT16 = "UInt16"
18
+ INT32 = "Int32"
19
+ UINT32 = "UInt32"
20
+ INT64 = "Int64"
21
+ UINT64 = "UInt64"
22
+ FLOAT32 = "Float32"
23
+ FLOAT64 = "Float64"
24
24
 
25
25
 
26
26
  def convert_ad_dtype_to_np(ad_dtype: ADBaseDataType) -> str:
27
27
  ad_dtype_to_np_dtype = {
28
- ADBaseDataType.Int8: "|i1",
29
- ADBaseDataType.UInt8: "|u1",
30
- ADBaseDataType.Int16: "<i2",
31
- ADBaseDataType.UInt16: "<u2",
32
- ADBaseDataType.Int32: "<i4",
33
- ADBaseDataType.UInt32: "<u4",
34
- ADBaseDataType.Int64: "<i8",
35
- ADBaseDataType.UInt64: "<u8",
36
- ADBaseDataType.Float32: "<f4",
37
- ADBaseDataType.Float64: "<f8",
28
+ ADBaseDataType.INT8: "|i1",
29
+ ADBaseDataType.UINT8: "|u1",
30
+ ADBaseDataType.INT16: "<i2",
31
+ ADBaseDataType.UINT16: "<u2",
32
+ ADBaseDataType.INT32: "<i4",
33
+ ADBaseDataType.UINT32: "<u4",
34
+ ADBaseDataType.INT64: "<i8",
35
+ ADBaseDataType.UINT64: "<u8",
36
+ ADBaseDataType.FLOAT32: "<f4",
37
+ ADBaseDataType.FLOAT64: "<f8",
38
38
  }
39
39
  return ad_dtype_to_np_dtype[ad_dtype]
40
40
 
41
41
 
42
42
  def convert_pv_dtype_to_np(datatype: str) -> str:
43
43
  _pvattribute_to_ad_datatype = {
44
- "DBR_SHORT": ADBaseDataType.Int16,
45
- "DBR_ENUM": ADBaseDataType.Int16,
46
- "DBR_INT": ADBaseDataType.Int32,
47
- "DBR_LONG": ADBaseDataType.Int32,
48
- "DBR_FLOAT": ADBaseDataType.Float32,
49
- "DBR_DOUBLE": ADBaseDataType.Float64,
44
+ "DBR_SHORT": ADBaseDataType.INT16,
45
+ "DBR_ENUM": ADBaseDataType.INT16,
46
+ "DBR_INT": ADBaseDataType.INT32,
47
+ "DBR_LONG": ADBaseDataType.INT32,
48
+ "DBR_FLOAT": ADBaseDataType.FLOAT32,
49
+ "DBR_DOUBLE": ADBaseDataType.FLOAT64,
50
50
  }
51
51
  if datatype in ["DBR_STRING", "DBR_CHAR"]:
52
52
  np_datatype = "s40"
@@ -62,9 +62,9 @@ def convert_pv_dtype_to_np(datatype: str) -> str:
62
62
 
63
63
  def convert_param_dtype_to_np(datatype: str) -> str:
64
64
  _paramattribute_to_ad_datatype = {
65
- "INT": ADBaseDataType.Int32,
66
- "INT64": ADBaseDataType.Int64,
67
- "DOUBLE": ADBaseDataType.Float64,
65
+ "INT": ADBaseDataType.INT32,
66
+ "INT64": ADBaseDataType.INT64,
67
+ "DOUBLE": ADBaseDataType.FLOAT64,
68
68
  }
69
69
  if datatype in ["STRING"]:
70
70
  np_datatype = "s40"
@@ -79,15 +79,15 @@ def convert_param_dtype_to_np(datatype: str) -> str:
79
79
 
80
80
 
81
81
  class FileWriteMode(StrictEnum):
82
- single = "Single"
83
- capture = "Capture"
84
- stream = "Stream"
82
+ SINGLE = "Single"
83
+ CAPTURE = "Capture"
84
+ STREAM = "Stream"
85
85
 
86
86
 
87
87
  class ImageMode(StrictEnum):
88
- single = "Single"
89
- multiple = "Multiple"
90
- continuous = "Continuous"
88
+ SINGLE = "Single"
89
+ MULTIPLE = "Multiple"
90
+ CONTINUOUS = "Continuous"
91
91
 
92
92
 
93
93
  class NDAttributeDataType(StrictEnum):
@@ -11,10 +11,10 @@ from ophyd_async.epics import adcore
11
11
  from ._kinetix_io import KinetixDriverIO, KinetixTriggerMode
12
12
 
13
13
  KINETIX_TRIGGER_MODE_MAP = {
14
- DetectorTrigger.internal: KinetixTriggerMode.internal,
15
- DetectorTrigger.constant_gate: KinetixTriggerMode.gate,
16
- DetectorTrigger.variable_gate: KinetixTriggerMode.gate,
17
- DetectorTrigger.edge_trigger: KinetixTriggerMode.edge,
14
+ DetectorTrigger.INTERNAL: KinetixTriggerMode.INTERNAL,
15
+ DetectorTrigger.CONSTANT_GATE: KinetixTriggerMode.GATE,
16
+ DetectorTrigger.VARIABLE_GATE: KinetixTriggerMode.GATE,
17
+ DetectorTrigger.EDGE_TRIGGER: KinetixTriggerMode.EDGE,
18
18
  }
19
19
 
20
20
 
@@ -33,11 +33,11 @@ class KinetixController(DetectorController):
33
33
  await asyncio.gather(
34
34
  self._drv.trigger_mode.set(KINETIX_TRIGGER_MODE_MAP[trigger_info.trigger]),
35
35
  self._drv.num_images.set(trigger_info.total_number_of_triggers),
36
- self._drv.image_mode.set(adcore.ImageMode.multiple),
36
+ self._drv.image_mode.set(adcore.ImageMode.MULTIPLE),
37
37
  )
38
38
  if trigger_info.livetime is not None and trigger_info.trigger not in [
39
- DetectorTrigger.variable_gate,
40
- DetectorTrigger.constant_gate,
39
+ DetectorTrigger.VARIABLE_GATE,
40
+ DetectorTrigger.CONSTANT_GATE,
41
41
  ]:
42
42
  await self._drv.acquire_time.set(trigger_info.livetime)
43
43
 
@@ -4,16 +4,16 @@ from ophyd_async.epics.core import epics_signal_rw_rbv
4
4
 
5
5
 
6
6
  class KinetixTriggerMode(StrictEnum):
7
- internal = "Internal"
8
- edge = "Rising Edge"
9
- gate = "Exp. Gate"
7
+ INTERNAL = "Internal"
8
+ EDGE = "Rising Edge"
9
+ GATE = "Exp. Gate"
10
10
 
11
11
 
12
12
  class KinetixReadoutMode(StrictEnum):
13
- sensitivity = 1
14
- speed = 2
15
- dynamic_range = 3
16
- sub_electron = 4
13
+ SENSITIVITY = 1
14
+ SPEED = 2
15
+ DYNAMIC_RANGE = 3
16
+ SUB_ELECTRON = 4
17
17
 
18
18
 
19
19
  class KinetixDriverIO(adcore.ADBaseIO):
@@ -17,10 +17,10 @@ class PilatusReadoutTime(float, Enum):
17
17
  """Pilatus readout time per model in ms"""
18
18
 
19
19
  # Cite: https://media.dectris.com/User_Manual-PILATUS2-V1_4.pdf
20
- pilatus2 = 2.28e-3
20
+ PILATUS2 = 2.28e-3
21
21
 
22
22
  # Cite: https://media.dectris.com/user-manual-pilatus3-2020.pdf
23
- pilatus3 = 0.95e-3
23
+ PILATUS3 = 0.95e-3
24
24
 
25
25
 
26
26
  class PilatusDetector(StandardDetector):
@@ -33,7 +33,7 @@ class PilatusDetector(StandardDetector):
33
33
  self,
34
34
  prefix: str,
35
35
  path_provider: PathProvider,
36
- readout_time: PilatusReadoutTime = PilatusReadoutTime.pilatus3,
36
+ readout_time: PilatusReadoutTime = PilatusReadoutTime.PILATUS3,
37
37
  drv_suffix: str = "cam1:",
38
38
  hdf_suffix: str = "HDF1:",
39
39
  name: str = "",
@@ -15,9 +15,9 @@ from ._pilatus_io import PilatusDriverIO, PilatusTriggerMode
15
15
 
16
16
  class PilatusController(DetectorController):
17
17
  _supported_trigger_types = {
18
- DetectorTrigger.internal: PilatusTriggerMode.internal,
19
- DetectorTrigger.constant_gate: PilatusTriggerMode.ext_enable,
20
- DetectorTrigger.variable_gate: PilatusTriggerMode.ext_enable,
18
+ DetectorTrigger.INTERNAL: PilatusTriggerMode.INTERNAL,
19
+ DetectorTrigger.CONSTANT_GATE: PilatusTriggerMode.EXT_ENABLE,
20
+ DetectorTrigger.VARIABLE_GATE: PilatusTriggerMode.EXT_ENABLE,
21
21
  }
22
22
 
23
23
  def __init__(
@@ -44,7 +44,7 @@ class PilatusController(DetectorController):
44
44
  if trigger_info.total_number_of_triggers == 0
45
45
  else trigger_info.total_number_of_triggers
46
46
  ),
47
- self._drv.image_mode.set(adcore.ImageMode.multiple),
47
+ self._drv.image_mode.set(adcore.ImageMode.MULTIPLE),
48
48
  )
49
49
 
50
50
  async def arm(self):
@@ -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):
@@ -26,14 +26,14 @@ class SimController(DetectorController):
26
26
 
27
27
  async def prepare(self, trigger_info: TriggerInfo):
28
28
  assert (
29
- trigger_info.trigger == DetectorTrigger.internal
29
+ trigger_info.trigger == DetectorTrigger.INTERNAL
30
30
  ), "fly scanning (i.e. external triggering) is not supported for this device"
31
31
  self.frame_timeout = (
32
32
  DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
33
33
  )
34
34
  await asyncio.gather(
35
35
  self.driver.num_images.set(trigger_info.total_number_of_triggers),
36
- self.driver.image_mode.set(adcore.ImageMode.multiple),
36
+ self.driver.image_mode.set(adcore.ImageMode.MULTIPLE),
37
37
  )
38
38
 
39
39
  async def arm(self):
@@ -11,17 +11,17 @@ from ophyd_async.epics import adcore
11
11
  from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
12
12
 
13
13
  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,
14
+ DetectorTrigger.INTERNAL: VimbaOnOff.OFF,
15
+ DetectorTrigger.CONSTANT_GATE: VimbaOnOff.ON,
16
+ DetectorTrigger.VARIABLE_GATE: VimbaOnOff.ON,
17
+ DetectorTrigger.EDGE_TRIGGER: VimbaOnOff.ON,
18
18
  }
19
19
 
20
20
  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,
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,
25
25
  }
26
26
 
27
27
 
@@ -41,17 +41,17 @@ class VimbaController(DetectorController):
41
41
  self._drv.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
42
42
  self._drv.exposure_mode.set(EXPOSE_OUT_MODE[trigger_info.trigger]),
43
43
  self._drv.num_images.set(trigger_info.total_number_of_triggers),
44
- self._drv.image_mode.set(adcore.ImageMode.multiple),
44
+ self._drv.image_mode.set(adcore.ImageMode.MULTIPLE),
45
45
  )
46
46
  if trigger_info.livetime is not None and trigger_info.trigger not in [
47
- DetectorTrigger.variable_gate,
48
- DetectorTrigger.constant_gate,
47
+ DetectorTrigger.VARIABLE_GATE,
48
+ DetectorTrigger.CONSTANT_GATE,
49
49
  ]:
50
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)
51
+ if trigger_info.trigger != DetectorTrigger.INTERNAL:
52
+ self._drv.trigger_source.set(VimbaTriggerSource.LINE1)
53
53
  else:
54
- self._drv.trigger_source.set(VimbaTriggerSource.freerun)
54
+ self._drv.trigger_source.set(VimbaTriggerSource.FREERUN)
55
55
 
56
56
  async def arm(self):
57
57
  self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
@@ -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):
@@ -94,6 +94,22 @@ class PvaConverter(Generic[SignalDatatypeT]):
94
94
  return value
95
95
 
96
96
 
97
+ class PvaLongStringConverter(PvaConverter[str]):
98
+ def __init__(self):
99
+ super().__init__(str)
100
+
101
+ def value(self, value: Any) -> Any:
102
+ # Value here is a null terminated array of ascii codes.
103
+ # We strip out the null terminator, and convert each code
104
+ # to the corresponding char, joining into a string
105
+ return value["value"].tobytes().rstrip(b"\0").decode()
106
+
107
+ def write_value(self, value: Any) -> Any:
108
+ # Inverse of reading - convert each character into it's ascii code,
109
+ # put into a list, and add null terminator.
110
+ return np.frombuffer(str(value).encode() + b"\0", dtype=np.int8)
111
+
112
+
97
113
  class DisconnectedPvaConverter(PvaConverter):
98
114
  def __getattribute__(self, __name: str) -> Any:
99
115
  raise NotImplementedError("No PV has been set as connect() has not been called")
@@ -252,6 +268,9 @@ def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverte
252
268
  elif datatype in (None, inferred_datatype):
253
269
  # If datatype matches what we are given then allow it and use inferred converter
254
270
  return converter_cls(inferred_datatype)
271
+ # Allow waveforms with FTVL=CHAR to be treated as str when requested
272
+ elif datatype is str and inferred_datatype == Array1D[np.int8]:
273
+ return PvaLongStringConverter()
255
274
  raise TypeError(
256
275
  f"{pv} with inferred datatype {format_datatype(inferred_datatype)}"
257
276
  f" from {typeid=} {specifier=}"