ophyd-async 0.8.0a6__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 (51) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +2 -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 +38 -136
  7. ophyd_async/core/_utils.py +11 -2
  8. ophyd_async/epics/adaravis/_aravis_controller.py +8 -8
  9. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  10. ophyd_async/epics/adcore/_core_io.py +21 -21
  11. ophyd_async/epics/adcore/_core_logic.py +3 -2
  12. ophyd_async/epics/adcore/_hdf_writer.py +6 -3
  13. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  14. ophyd_async/epics/adcore/_utils.py +35 -35
  15. ophyd_async/epics/adkinetix/_kinetix_controller.py +7 -7
  16. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  17. ophyd_async/epics/adpilatus/_pilatus.py +3 -3
  18. ophyd_async/epics/adpilatus/_pilatus_controller.py +4 -4
  19. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  20. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  21. ophyd_async/epics/advimba/_vimba_controller.py +14 -14
  22. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  23. ophyd_async/epics/core/_p4p.py +19 -0
  24. ophyd_async/epics/core/_pvi_connector.py +4 -2
  25. ophyd_async/epics/core/_signal.py +9 -2
  26. ophyd_async/epics/core/_util.py +9 -0
  27. ophyd_async/epics/demo/_mover.py +2 -2
  28. ophyd_async/epics/demo/_sensor.py +2 -2
  29. ophyd_async/epics/eiger/_eiger_controller.py +4 -4
  30. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  31. ophyd_async/epics/motor.py +8 -5
  32. ophyd_async/epics/testing/_example_ioc.py +5 -3
  33. ophyd_async/epics/testing/test_records.db +6 -0
  34. ophyd_async/fastcs/core.py +2 -2
  35. ophyd_async/fastcs/panda/_block.py +9 -9
  36. ophyd_async/fastcs/panda/_control.py +2 -2
  37. ophyd_async/fastcs/panda/_hdf_panda.py +4 -1
  38. ophyd_async/fastcs/panda/_trigger.py +7 -7
  39. ophyd_async/plan_stubs/_fly.py +1 -1
  40. ophyd_async/sim/demo/_sim_motor.py +34 -32
  41. ophyd_async/tango/core/_tango_transport.py +1 -1
  42. ophyd_async/testing/__init__.py +33 -0
  43. ophyd_async/testing/_assert.py +128 -0
  44. ophyd_async/{core → testing}/_mock_signal_utils.py +12 -8
  45. ophyd_async/testing/_wait_for_pending.py +22 -0
  46. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/METADATA +3 -1
  47. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/RECORD +51 -48
  48. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/LICENSE +0 -0
  49. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/WHEEL +0 -0
  50. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/entry_points.txt +0 -0
  51. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -102,7 +102,8 @@ async def start_acquiring_driver_and_ensure_status(
102
102
  state = await driver.detector_state.get_value()
103
103
  if state not in good_states:
104
104
  raise ValueError(
105
- 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}"
106
107
  )
107
108
 
108
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=}"
@@ -32,10 +32,11 @@ def _get_signal_details(entry: Entry) -> tuple[type[Signal], str, str]:
32
32
 
33
33
 
34
34
  class PviDeviceConnector(DeviceConnector):
35
- def __init__(self, prefix: str = "") -> None:
35
+ def __init__(self, prefix: str = "", error_hint: str = "") -> None:
36
36
  # TODO: what happens if we get a leading "pva://" here?
37
37
  self.prefix = prefix
38
38
  self.pvi_pv = prefix + "PVI"
39
+ self.error_hint = error_hint
39
40
 
40
41
  def create_children_from_annotations(self, device: Device):
41
42
  if not hasattr(self, "filler"):
@@ -85,7 +86,8 @@ class PviDeviceConnector(DeviceConnector):
85
86
  if e:
86
87
  self._fill_child(name, e, i)
87
88
  # Check that all the requested children have been filled
88
- self.filler.check_filled(f"{self.pvi_pv}: {entries}")
89
+ suffix = f"\n{self.error_hint}" if self.error_hint else ""
90
+ self.filler.check_filled(f"{self.pvi_pv}: {entries}{suffix}")
89
91
  # Set the name of the device to name all children
90
92
  device.set_name(device.name)
91
93
  return await super().connect_real(device, timeout, force_reconnect)
@@ -14,7 +14,7 @@ from ophyd_async.core import (
14
14
  get_unique,
15
15
  )
16
16
 
17
- from ._util import EpicsSignalBackend
17
+ from ._util import EpicsSignalBackend, get_pv_basename_and_field
18
18
 
19
19
 
20
20
  class EpicsProtocol(Enum):
@@ -124,7 +124,14 @@ def epics_signal_rw_rbv(
124
124
  read_suffix:
125
125
  Append this suffix to the write pv to create the readback pv
126
126
  """
127
- return epics_signal_rw(datatype, f"{write_pv}{read_suffix}", write_pv, name)
127
+
128
+ base_pv, field = get_pv_basename_and_field(write_pv)
129
+ if field is not None:
130
+ read_pv = f"{base_pv}{read_suffix}.{field}"
131
+ else:
132
+ read_pv = f"{write_pv}{read_suffix}"
133
+
134
+ return epics_signal_rw(datatype, read_pv, write_pv, name)
128
135
 
129
136
 
130
137
  def epics_signal_r(
@@ -12,6 +12,15 @@ from ophyd_async.core import (
12
12
  )
13
13
 
14
14
 
15
+ def get_pv_basename_and_field(pv: str) -> tuple[str, str | None]:
16
+ """Simple utility function for extracting base pv name without field"""
17
+
18
+ if "." in pv:
19
+ return (pv.split(".", -1)[0], pv.split(".", -1)[1])
20
+ else:
21
+ return (pv, None)
22
+
23
+
15
24
  def get_supported_values(
16
25
  pv: str,
17
26
  datatype: type,
@@ -37,8 +37,8 @@ class Mover(StandardReadable, Movable, Stoppable):
37
37
 
38
38
  super().__init__(name=name)
39
39
 
40
- def set_name(self, name: str):
41
- super().set_name(name)
40
+ def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
41
+ super().set_name(name, child_name_separator=child_name_separator)
42
42
  # Readback should be named the same as its parent in read()
43
43
  self.readback.set_name(name)
44
44
 
@@ -15,9 +15,9 @@ class EnergyMode(StrictEnum):
15
15
  """Energy mode for `Sensor`"""
16
16
 
17
17
  #: Low energy mode
18
- low = "Low Energy"
18
+ LOW = "Low Energy"
19
19
  #: High energy mode
20
- high = "High Energy"
20
+ HIGH = "High Energy"
21
21
 
22
22
 
23
23
  class Sensor(StandardReadable, EpicsDevice):
@@ -11,10 +11,10 @@ from ophyd_async.core import (
11
11
  from ._eiger_io import EigerDriverIO, EigerTriggerMode
12
12
 
13
13
  EIGER_TRIGGER_MODE_MAP = {
14
- DetectorTrigger.internal: EigerTriggerMode.internal,
15
- DetectorTrigger.constant_gate: EigerTriggerMode.gate,
16
- DetectorTrigger.variable_gate: EigerTriggerMode.gate,
17
- DetectorTrigger.edge_trigger: EigerTriggerMode.edge,
14
+ DetectorTrigger.INTERNAL: EigerTriggerMode.INTERNAL,
15
+ DetectorTrigger.CONSTANT_GATE: EigerTriggerMode.GATE,
16
+ DetectorTrigger.VARIABLE_GATE: EigerTriggerMode.GATE,
17
+ DetectorTrigger.EDGE_TRIGGER: EigerTriggerMode.EDGE,
18
18
  }
19
19
 
20
20
 
@@ -3,9 +3,9 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw_rbv, epics_si
3
3
 
4
4
 
5
5
  class EigerTriggerMode(StrictEnum):
6
- internal = "ints"
7
- edge = "exts"
8
- gate = "exte"
6
+ INTERNAL = "ints"
7
+ EDGE = "exts"
8
+ GATE = "exte"
9
9
 
10
10
 
11
11
  class EigerDriverIO(Device):
@@ -20,7 +20,7 @@ from ophyd_async.core import (
20
20
  observe_value,
21
21
  )
22
22
  from ophyd_async.core import StandardReadableFormat as Format
23
- from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
23
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
24
24
 
25
25
 
26
26
  class MotorLimitsException(Exception):
@@ -76,7 +76,10 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
76
76
  self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
77
77
  self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
78
78
 
79
- self.motor_stop = epics_signal_x(prefix + ".STOP")
79
+ # Note:cannot use epics_signal_x here, as the motor record specifies that
80
+ # we must write 1 to stop the motor. Simply processing the record is not
81
+ # sufficient.
82
+ self.motor_stop = epics_signal_w(int, prefix + ".STOP")
80
83
  # Whether set() should complete successfully or not
81
84
  self._set_success = True
82
85
 
@@ -91,8 +94,8 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
91
94
 
92
95
  super().__init__(name=name)
93
96
 
94
- def set_name(self, name: str):
95
- super().set_name(name)
97
+ def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
98
+ super().set_name(name, child_name_separator=child_name_separator)
96
99
  # Readback should be named the same as its parent in read()
97
100
  self.user_readback.set_name(name)
98
101
 
@@ -178,7 +181,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
178
181
  self._set_success = success
179
182
  # Put with completion will never complete as we are waiting for completion on
180
183
  # the move above, so need to pass wait=False
181
- await self.motor_stop.trigger(wait=False)
184
+ await self.motor_stop.set(1, wait=False)
182
185
 
183
186
  async def _prepare_velocity(
184
187
  self, start_position: float, end_position: float, time_for_move: float
@@ -23,9 +23,9 @@ PVA_RECORDS = str(Path(__file__).parent / "test_records_pva.db")
23
23
 
24
24
 
25
25
  class ExampleEnum(StrictEnum):
26
- a = "Aaa"
27
- b = "Bbb"
28
- c = "Ccc"
26
+ A = "Aaa"
27
+ B = "Bbb"
28
+ C = "Ccc"
29
29
 
30
30
 
31
31
  class ExampleTable(Table):
@@ -40,6 +40,8 @@ class ExampleCaDevice(EpicsDevice):
40
40
  my_int: A[SignalRW[int], PvSuffix("int")]
41
41
  my_float: A[SignalRW[float], PvSuffix("float")]
42
42
  my_str: A[SignalRW[str], PvSuffix("str")]
43
+ longstr: A[SignalRW[str], PvSuffix("longstr")]
44
+ longstr2: A[SignalRW[str], PvSuffix("longstr2")]
43
45
  my_bool: A[SignalRW[bool], PvSuffix("bool")]
44
46
  enum: A[SignalRW[ExampleEnum], PvSuffix("enum")]
45
47
  enum2: A[SignalRW[ExampleEnum], PvSuffix("enum2")]
@@ -150,3 +150,9 @@ record(lsi, "$(device)longstr2") {
150
150
  field(INP, {const:"a string that is just longer than forty characters"})
151
151
  field(PINI, "YES")
152
152
  }
153
+
154
+ record(calc, "$(device)ticking") {
155
+ field(INPA, "$(device)ticking")
156
+ field(CALC, "A+1")
157
+ field(SCAN, ".1 second")
158
+ }