ophyd-async 0.3.4a1__py3-none-any.whl → 0.5.0__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 (103) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +86 -63
  3. ophyd_async/core/{detector.py → _detector.py} +18 -23
  4. ophyd_async/core/{device.py → _device.py} +19 -7
  5. ophyd_async/core/{device_save_loader.py → _device_save_loader.py} +3 -3
  6. ophyd_async/core/{flyer.py → _flyer.py} +6 -8
  7. ophyd_async/core/_hdf_dataset.py +97 -0
  8. ophyd_async/{log.py → core/_log.py} +11 -3
  9. ophyd_async/core/{mock_signal_backend.py → _mock_signal_backend.py} +3 -3
  10. ophyd_async/core/{mock_signal_utils.py → _mock_signal_utils.py} +3 -4
  11. ophyd_async/{protocols.py → core/_protocol.py} +1 -1
  12. ophyd_async/core/_providers.py +186 -24
  13. ophyd_async/core/{standard_readable.py → _readable.py} +6 -16
  14. ophyd_async/core/{signal.py → _signal.py} +39 -16
  15. ophyd_async/core/{signal_backend.py → _signal_backend.py} +4 -13
  16. ophyd_async/core/{soft_signal_backend.py → _soft_signal_backend.py} +24 -18
  17. ophyd_async/core/{async_status.py → _status.py} +3 -11
  18. ophyd_async/epics/adaravis/__init__.py +9 -0
  19. ophyd_async/epics/{areadetector/aravis.py → adaravis/_aravis.py} +12 -14
  20. ophyd_async/epics/{areadetector/controllers/aravis_controller.py → adaravis/_aravis_controller.py} +8 -10
  21. ophyd_async/epics/{areadetector/drivers/aravis_driver.py → adaravis/_aravis_io.py} +6 -3
  22. ophyd_async/epics/adcore/__init__.py +36 -0
  23. ophyd_async/epics/adcore/_core_io.py +114 -0
  24. ophyd_async/epics/{areadetector/drivers/ad_base.py → adcore/_core_logic.py} +17 -52
  25. ophyd_async/epics/{areadetector/writers/hdf_writer.py → adcore/_hdf_writer.py} +36 -18
  26. ophyd_async/epics/{areadetector/single_trigger_det.py → adcore/_single_trigger.py} +5 -6
  27. ophyd_async/epics/{areadetector/utils.py → adcore/_utils.py} +29 -0
  28. ophyd_async/epics/adkinetix/__init__.py +9 -0
  29. ophyd_async/epics/{areadetector/kinetix.py → adkinetix/_kinetix.py} +12 -14
  30. ophyd_async/epics/{areadetector/controllers/kinetix_controller.py → adkinetix/_kinetix_controller.py} +6 -9
  31. ophyd_async/epics/{areadetector/drivers/kinetix_driver.py → adkinetix/_kinetix_io.py} +5 -4
  32. ophyd_async/epics/adpilatus/__init__.py +11 -0
  33. ophyd_async/epics/{areadetector/pilatus.py → adpilatus/_pilatus.py} +12 -16
  34. ophyd_async/epics/{areadetector/controllers/pilatus_controller.py → adpilatus/_pilatus_controller.py} +14 -16
  35. ophyd_async/epics/{areadetector/drivers/pilatus_driver.py → adpilatus/_pilatus_io.py} +5 -3
  36. ophyd_async/epics/adsimdetector/__init__.py +7 -0
  37. ophyd_async/epics/adsimdetector/_sim.py +34 -0
  38. ophyd_async/epics/{areadetector/controllers/ad_sim_controller.py → adsimdetector/_sim_controller.py} +8 -14
  39. ophyd_async/epics/advimba/__init__.py +9 -0
  40. ophyd_async/epics/advimba/_vimba.py +43 -0
  41. ophyd_async/epics/{areadetector/controllers/vimba_controller.py → advimba/_vimba_controller.py} +6 -14
  42. ophyd_async/epics/{areadetector/drivers/vimba_driver.py → advimba/_vimba_io.py} +5 -4
  43. ophyd_async/epics/demo/__init__.py +9 -132
  44. ophyd_async/epics/demo/_mover.py +97 -0
  45. ophyd_async/epics/demo/_sensor.py +36 -0
  46. ophyd_async/epics/motor.py +228 -0
  47. ophyd_async/epics/pvi/__init__.py +2 -2
  48. ophyd_async/epics/pvi/{pvi.py → _pvi.py} +17 -14
  49. ophyd_async/epics/signal/__init__.py +7 -1
  50. ophyd_async/epics/{_backend → signal}/_aioca.py +6 -2
  51. ophyd_async/epics/{_backend/common.py → signal/_common.py} +4 -2
  52. ophyd_async/epics/signal/_epics_transport.py +3 -3
  53. ophyd_async/epics/{_backend → signal}/_p4p.py +53 -4
  54. ophyd_async/epics/signal/{signal.py → _signal.py} +10 -9
  55. ophyd_async/fastcs/odin/__init__.py +0 -0
  56. ophyd_async/{panda → fastcs/panda}/__init__.py +28 -9
  57. ophyd_async/{panda → fastcs/panda}/_common_blocks.py +24 -3
  58. ophyd_async/{panda → fastcs/panda}/_hdf_panda.py +6 -9
  59. ophyd_async/{panda/writers → fastcs/panda}/_hdf_writer.py +24 -14
  60. ophyd_async/{panda → fastcs/panda}/_panda_controller.py +2 -1
  61. ophyd_async/{panda → fastcs/panda}/_table.py +20 -18
  62. ophyd_async/fastcs/panda/_trigger.py +90 -0
  63. ophyd_async/plan_stubs/__init__.py +2 -2
  64. ophyd_async/plan_stubs/_ensure_connected.py +26 -0
  65. ophyd_async/plan_stubs/{fly.py → _fly.py} +67 -12
  66. ophyd_async/sim/__init__.py +0 -11
  67. ophyd_async/sim/demo/__init__.py +18 -2
  68. ophyd_async/sim/demo/_pattern_detector/__init__.py +13 -0
  69. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +42 -0
  70. ophyd_async/sim/{sim_pattern_detector_control.py → demo/_pattern_detector/_pattern_detector_controller.py} +6 -7
  71. ophyd_async/sim/{sim_pattern_detector_writer.py → demo/_pattern_detector/_pattern_detector_writer.py} +12 -8
  72. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +211 -0
  73. ophyd_async/sim/demo/{sim_motor.py → _sim_motor.py} +7 -5
  74. ophyd_async/sim/testing/__init__.py +0 -0
  75. ophyd_async/tango/__init__.py +0 -0
  76. {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.5.0.dist-info}/METADATA +7 -2
  77. ophyd_async-0.5.0.dist-info/RECORD +89 -0
  78. {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.5.0.dist-info}/WHEEL +1 -1
  79. ophyd_async/epics/areadetector/__init__.py +0 -23
  80. ophyd_async/epics/areadetector/controllers/__init__.py +0 -5
  81. ophyd_async/epics/areadetector/drivers/__init__.py +0 -23
  82. ophyd_async/epics/areadetector/vimba.py +0 -43
  83. ophyd_async/epics/areadetector/writers/__init__.py +0 -5
  84. ophyd_async/epics/areadetector/writers/_hdfdataset.py +0 -10
  85. ophyd_async/epics/areadetector/writers/_hdffile.py +0 -54
  86. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +0 -40
  87. ophyd_async/epics/areadetector/writers/nd_plugin.py +0 -38
  88. ophyd_async/epics/demo/demo_ad_sim_detector.py +0 -35
  89. ophyd_async/epics/motion/__init__.py +0 -3
  90. ophyd_async/epics/motion/motor.py +0 -97
  91. ophyd_async/panda/_trigger.py +0 -39
  92. ophyd_async/panda/writers/__init__.py +0 -3
  93. ophyd_async/panda/writers/_panda_hdf_file.py +0 -54
  94. ophyd_async/plan_stubs/ensure_connected.py +0 -22
  95. ophyd_async/sim/pattern_generator.py +0 -318
  96. ophyd_async/sim/sim_pattern_generator.py +0 -35
  97. ophyd_async-0.3.4a1.dist-info/RECORD +0 -86
  98. /ophyd_async/core/{utils.py → _utils.py} +0 -0
  99. /ophyd_async/{epics/_backend → fastcs}/__init__.py +0 -0
  100. /ophyd_async/{panda → fastcs/panda}/_utils.py +0 -0
  101. {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.5.0.dist-info}/LICENSE +0 -0
  102. {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.5.0.dist-info}/entry_points.txt +0 -0
  103. {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,228 @@
1
+ import asyncio
2
+ from typing import Optional
3
+
4
+ from bluesky.protocols import Flyable, Movable, Preparable, Stoppable
5
+ from pydantic import BaseModel, Field
6
+
7
+ from ophyd_async.core import (
8
+ DEFAULT_TIMEOUT,
9
+ AsyncStatus,
10
+ CalculatableTimeout,
11
+ CalculateTimeout,
12
+ ConfigSignal,
13
+ HintedSignal,
14
+ StandardReadable,
15
+ WatchableAsyncStatus,
16
+ WatcherUpdate,
17
+ observe_value,
18
+ )
19
+ from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x
20
+
21
+
22
+ class MotorLimitsException(Exception):
23
+ pass
24
+
25
+
26
+ class InvalidFlyMotorException(Exception):
27
+ pass
28
+
29
+
30
+ DEFAULT_MOTOR_FLY_TIMEOUT = 60
31
+ DEFAULT_WATCHER_UPDATE_FREQUENCY = 0.2
32
+
33
+
34
+ class FlyMotorInfo(BaseModel):
35
+ """Minimal set of information required to fly a motor:"""
36
+
37
+ #: Absolute position of the motor once it finishes accelerating to desired
38
+ #: velocity, in motor EGUs
39
+ start_position: float = Field(frozen=True)
40
+
41
+ #: Absolute position of the motor once it begins decelerating from desired
42
+ #: velocity, in EGUs
43
+ end_position: float = Field(frozen=True)
44
+
45
+ #: Time taken for the motor to get from start_position to end_position, excluding
46
+ #: run-up and run-down, in seconds.
47
+ time_for_move: float = Field(frozen=True, gt=0)
48
+
49
+ #: Maximum time for the complete motor move, including run up and run down.
50
+ #: Defaults to `time_for_move` + run up and run down times + 10s.
51
+ timeout: CalculatableTimeout = Field(frozen=True, default=CalculateTimeout)
52
+
53
+
54
+ class Motor(StandardReadable, Movable, Stoppable, Flyable, Preparable):
55
+ """Device that moves a motor record"""
56
+
57
+ def __init__(self, prefix: str, name="") -> None:
58
+ # Define some signals
59
+ with self.add_children_as_readables(ConfigSignal):
60
+ self.motor_egu = epics_signal_r(str, prefix + ".EGU")
61
+ self.velocity = epics_signal_rw(float, prefix + ".VELO")
62
+
63
+ with self.add_children_as_readables(HintedSignal):
64
+ self.user_readback = epics_signal_r(float, prefix + ".RBV")
65
+
66
+ self.user_setpoint = epics_signal_rw(float, prefix + ".VAL")
67
+ self.max_velocity = epics_signal_r(float, prefix + ".VMAX")
68
+ self.acceleration_time = epics_signal_rw(float, prefix + ".ACCL")
69
+ self.precision = epics_signal_r(int, prefix + ".PREC")
70
+ self.deadband = epics_signal_r(float, prefix + ".RDBD")
71
+ self.motor_done_move = epics_signal_r(int, prefix + ".DMOV")
72
+ self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
73
+ self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
74
+
75
+ self.motor_stop = epics_signal_x(prefix + ".STOP")
76
+ # Whether set() should complete successfully or not
77
+ self._set_success = True
78
+
79
+ # end_position of a fly move, with run_up_distance added on.
80
+ self._fly_completed_position: Optional[float] = None
81
+
82
+ # Set on kickoff(), complete when motor reaches self._fly_completed_position
83
+ self._fly_status: Optional[WatchableAsyncStatus] = None
84
+
85
+ # Set during prepare
86
+ self._fly_timeout: Optional[CalculatableTimeout] = CalculateTimeout
87
+
88
+ super().__init__(name=name)
89
+
90
+ def set_name(self, name: str):
91
+ super().set_name(name)
92
+ # Readback should be named the same as its parent in read()
93
+ self.user_readback.set_name(name)
94
+
95
+ @AsyncStatus.wrap
96
+ async def prepare(self, value: FlyMotorInfo):
97
+ """Calculate required velocity and run-up distance, then if motor limits aren't
98
+ breached, move to start position minus run-up distance"""
99
+
100
+ self._fly_timeout = value.timeout
101
+
102
+ # Velocity, at which motor travels from start_position to end_position, in motor
103
+ # egu/s.
104
+ fly_velocity = await self._prepare_velocity(
105
+ value.start_position,
106
+ value.end_position,
107
+ value.time_for_move,
108
+ )
109
+
110
+ # start_position with run_up_distance added on.
111
+ fly_prepared_position = await self._prepare_motor_path(
112
+ abs(fly_velocity), value.start_position, value.end_position
113
+ )
114
+
115
+ await self.set(fly_prepared_position)
116
+ await self.velocity.set(fly_velocity)
117
+
118
+ @AsyncStatus.wrap
119
+ async def kickoff(self):
120
+ """Begin moving motor from prepared position to final position."""
121
+ assert (
122
+ self._fly_completed_position
123
+ ), "Motor must be prepared before attempting to kickoff"
124
+
125
+ self._fly_status = self.set(
126
+ self._fly_completed_position, timeout=self._fly_timeout
127
+ )
128
+
129
+ def complete(self) -> WatchableAsyncStatus:
130
+ """Mark as complete once motor reaches completed position."""
131
+ assert self._fly_status, "kickoff not called"
132
+ return self._fly_status
133
+
134
+ @WatchableAsyncStatus.wrap
135
+ async def set(
136
+ self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
137
+ ):
138
+ self._set_success = True
139
+ (
140
+ old_position,
141
+ units,
142
+ precision,
143
+ velocity,
144
+ acceleration_time,
145
+ ) = await asyncio.gather(
146
+ self.user_setpoint.get_value(),
147
+ self.motor_egu.get_value(),
148
+ self.precision.get_value(),
149
+ self.velocity.get_value(),
150
+ self.acceleration_time.get_value(),
151
+ )
152
+ if timeout is CalculateTimeout:
153
+ assert velocity > 0, "Motor has zero velocity"
154
+ timeout = (
155
+ abs(new_position - old_position) / velocity
156
+ + 2 * acceleration_time
157
+ + DEFAULT_TIMEOUT
158
+ )
159
+ move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
160
+ async for current_position in observe_value(
161
+ self.user_readback, done_status=move_status
162
+ ):
163
+ yield WatcherUpdate(
164
+ current=current_position,
165
+ initial=old_position,
166
+ target=new_position,
167
+ name=self.name,
168
+ unit=units,
169
+ precision=precision,
170
+ )
171
+ if not self._set_success:
172
+ raise RuntimeError("Motor was stopped")
173
+
174
+ async def stop(self, success=False):
175
+ self._set_success = success
176
+ # Put with completion will never complete as we are waiting for completion on
177
+ # the move above, so need to pass wait=False
178
+ await self.motor_stop.trigger(wait=False)
179
+
180
+ async def _prepare_velocity(
181
+ self, start_position: float, end_position: float, time_for_move: float
182
+ ) -> float:
183
+ fly_velocity = (end_position - start_position) / time_for_move
184
+ max_speed, egu = await asyncio.gather(
185
+ self.max_velocity.get_value(), self.motor_egu.get_value()
186
+ )
187
+ if abs(fly_velocity) > max_speed:
188
+ raise MotorLimitsException(
189
+ f"Motor speed of {abs(fly_velocity)} {egu}/s was requested for a motor "
190
+ f" with max speed of {max_speed} {egu}/s"
191
+ )
192
+ # move to prepare position at maximum velocity
193
+ await self.velocity.set(abs(max_speed))
194
+ return fly_velocity
195
+
196
+ async def _prepare_motor_path(
197
+ self, fly_velocity: float, start_position: float, end_position: float
198
+ ) -> float:
199
+ # Distance required for motor to accelerate from stationary to fly_velocity, and
200
+ # distance required for motor to decelerate from fly_velocity to stationary
201
+ run_up_distance = (
202
+ (await self.acceleration_time.get_value()) * fly_velocity * 0.5
203
+ )
204
+
205
+ self._fly_completed_position = end_position + run_up_distance
206
+
207
+ # Prepared position not used after prepare, so no need to store in self
208
+ fly_prepared_position = start_position - run_up_distance
209
+
210
+ motor_lower_limit, motor_upper_limit, egu = await asyncio.gather(
211
+ self.low_limit_travel.get_value(),
212
+ self.high_limit_travel.get_value(),
213
+ self.motor_egu.get_value(),
214
+ )
215
+
216
+ if (
217
+ not motor_upper_limit >= fly_prepared_position >= motor_lower_limit
218
+ or not motor_upper_limit
219
+ >= self._fly_completed_position
220
+ >= motor_lower_limit
221
+ ):
222
+ raise MotorLimitsException(
223
+ f"Motor trajectory for requested fly is from "
224
+ f"{fly_prepared_position}{egu} to "
225
+ f"{self._fly_completed_position}{egu} but motor limits are "
226
+ f"{motor_lower_limit}{egu} <= x <= {motor_upper_limit}{egu} "
227
+ )
228
+ return fly_prepared_position
@@ -1,3 +1,3 @@
1
- from .pvi import PVIEntry, create_children_from_annotations, fill_pvi_entries
1
+ from ._pvi import create_children_from_annotations, fill_pvi_entries
2
2
 
3
- __all__ = ["PVIEntry", "fill_pvi_entries", "create_children_from_annotations"]
3
+ __all__ = ["fill_pvi_entries", "create_children_from_annotations"]
@@ -10,25 +10,28 @@ from typing import (
10
10
  Optional,
11
11
  Tuple,
12
12
  Type,
13
- TypeVar,
14
13
  Union,
15
14
  get_args,
16
15
  get_origin,
17
16
  get_type_hints,
18
17
  )
19
18
 
20
- from ophyd_async.core import Device, DeviceVector, SoftSignalBackend
21
- from ophyd_async.core.signal import Signal
22
- from ophyd_async.core.utils import DEFAULT_TIMEOUT
23
- from ophyd_async.epics._backend._p4p import PvaSignalBackend
24
- from ophyd_async.epics.signal.signal import (
19
+ from ophyd_async.core import (
20
+ DEFAULT_TIMEOUT,
21
+ Device,
22
+ DeviceVector,
23
+ Signal,
24
+ SoftSignalBackend,
25
+ T,
26
+ )
27
+ from ophyd_async.epics.signal import (
28
+ PvaSignalBackend,
25
29
  epics_signal_r,
26
30
  epics_signal_rw,
27
31
  epics_signal_w,
28
32
  epics_signal_x,
29
33
  )
30
34
 
31
- T = TypeVar("T")
32
35
  Access = FrozenSet[
33
36
  Union[Literal["r"], Literal["w"], Literal["rw"], Literal["x"], Literal["d"]]
34
37
  ]
@@ -74,19 +77,19 @@ def _strip_device_vector(field: Union[Type[Device]]) -> Tuple[bool, Type[Device]
74
77
 
75
78
 
76
79
  @dataclass
77
- class PVIEntry:
80
+ class _PVIEntry:
78
81
  """
79
82
  A dataclass to represent a single entry in the PVI table.
80
83
  This could either be a signal or a sub-table.
81
84
  """
82
85
 
83
- sub_entries: Dict[str, Union[Dict[int, "PVIEntry"], "PVIEntry"]]
86
+ sub_entries: Dict[str, Union[Dict[int, "_PVIEntry"], "_PVIEntry"]]
84
87
  pvi_pv: Optional[str] = None
85
88
  device: Optional[Device] = None
86
89
  common_device_type: Optional[Type[Device]] = None
87
90
 
88
91
 
89
- def _verify_common_blocks(entry: PVIEntry, common_device: Type[Device]):
92
+ def _verify_common_blocks(entry: _PVIEntry, common_device: Type[Device]):
90
93
  if not entry.sub_entries:
91
94
  return
92
95
  common_sub_devices = get_type_hints(common_device)
@@ -205,7 +208,7 @@ def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None):
205
208
  sub_device.parent = device
206
209
 
207
210
 
208
- async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT):
211
+ async def _get_pvi_entries(entry: _PVIEntry, timeout=DEFAULT_TIMEOUT):
209
212
  if not entry.pvi_pv or not entry.pvi_pv.endswith(":PVI"):
210
213
  raise RuntimeError("Top level entry must be a pvi table")
211
214
 
@@ -235,7 +238,7 @@ async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT):
235
238
  else:
236
239
  device = getattr(entry.device, sub_name, device_type())
237
240
 
238
- sub_entry = PVIEntry(
241
+ sub_entry = _PVIEntry(
239
242
  device=device, common_device_type=device_type, sub_entries={}
240
243
  )
241
244
 
@@ -257,7 +260,7 @@ async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT):
257
260
  _verify_common_blocks(entry, entry.common_device_type)
258
261
 
259
262
 
260
- def _set_device_attributes(entry: PVIEntry):
263
+ def _set_device_attributes(entry: _PVIEntry):
261
264
  for sub_name, sub_entry in entry.sub_entries.items():
262
265
  if isinstance(sub_entry, dict):
263
266
  sub_device = DeviceVector() # type: ignore
@@ -289,7 +292,7 @@ async def fill_pvi_entries(
289
292
  _mock_common_blocks(device)
290
293
  else:
291
294
  # check the pvi table for devices and fill the device with them
292
- root_entry = PVIEntry(
295
+ root_entry = _PVIEntry(
293
296
  pvi_pv=root_pv,
294
297
  device=device,
295
298
  common_device_type=type(device),
@@ -1,4 +1,6 @@
1
- from .signal import (
1
+ from ._common import LimitPair, Limits, get_supported_values
2
+ from ._p4p import PvaSignalBackend
3
+ from ._signal import (
2
4
  epics_signal_r,
3
5
  epics_signal_rw,
4
6
  epics_signal_rw_rbv,
@@ -7,6 +9,10 @@ from .signal import (
7
9
  )
8
10
 
9
11
  __all__ = [
12
+ "get_supported_values",
13
+ "LimitPair",
14
+ "Limits",
15
+ "PvaSignalBackend",
10
16
  "epics_signal_r",
11
17
  "epics_signal_rw",
12
18
  "epics_signal_rw_rbv",
@@ -21,6 +21,8 @@ from bluesky.protocols import DataKey, Dtype, Reading
21
21
  from epicscorelibs.ca import dbr
22
22
 
23
23
  from ophyd_async.core import (
24
+ DEFAULT_TIMEOUT,
25
+ NotConnected,
24
26
  ReadingValueCallback,
25
27
  SignalBackend,
26
28
  T,
@@ -28,9 +30,8 @@ from ophyd_async.core import (
28
30
  get_unique,
29
31
  wait_for_connection,
30
32
  )
31
- from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected
32
33
 
33
- from .common import LimitPair, Limits, common_meta, get_supported_values
34
+ from ._common import LimitPair, Limits, common_meta, get_supported_values
34
35
 
35
36
  dbr_to_dtype: Dict[Dbr, Dtype] = {
36
37
  dbr.DBR_STRING: "string",
@@ -66,9 +67,12 @@ def _data_key_from_augmented_value(
66
67
  scalar = value.element_count == 1
67
68
  dtype = dtype or dbr_to_dtype[value.datatype]
68
69
 
70
+ dtype_numpy = np.dtype(dbr.DbrCodeToType[value.datatype].dtype).descr[0][1]
71
+
69
72
  d = DataKey(
70
73
  source=source,
71
74
  dtype=dtype if scalar else "array",
75
+ dtype_numpy=dtype_numpy,
72
76
  # strictly value.element_count >= len(value)
73
77
  shape=[] if scalar else [len(value)],
74
78
  )
@@ -1,8 +1,10 @@
1
1
  import inspect
2
2
  from enum import Enum
3
- from typing import Dict, Optional, Tuple, Type, TypedDict
3
+ from typing import Dict, Optional, Tuple, Type
4
4
 
5
- from ophyd_async.core.signal_backend import RuntimeSubsetEnum
5
+ from typing_extensions import TypedDict
6
+
7
+ from ophyd_async.core import RuntimeSubsetEnum
6
8
 
7
9
  common_meta = {
8
10
  "units",
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from enum import Enum
6
6
 
7
7
  try:
8
- from .._backend._aioca import CaSignalBackend
8
+ from ._aioca import CaSignalBackend
9
9
  except ImportError as ca_error:
10
10
 
11
11
  class CaSignalBackend: # type: ignore
@@ -14,7 +14,7 @@ except ImportError as ca_error:
14
14
 
15
15
 
16
16
  try:
17
- from .._backend._p4p import PvaSignalBackend
17
+ from ._p4p import PvaSignalBackend
18
18
  except ImportError as pva_error:
19
19
 
20
20
  class PvaSignalBackend: # type: ignore
@@ -22,7 +22,7 @@ except ImportError as pva_error:
22
22
  raise NotImplementedError("PVA support not available") from pva_error
23
23
 
24
24
 
25
- class EpicsTransport(Enum):
25
+ class _EpicsTransport(Enum):
26
26
  """The sorts of transport EPICS support"""
27
27
 
28
28
  #: Use Channel Access (using aioca library)
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import atexit
3
+ import inspect
3
4
  import logging
4
5
  import time
5
6
  from dataclasses import dataclass
@@ -12,16 +13,18 @@ from p4p import Value
12
13
  from p4p.client.asyncio import Context, Subscription
13
14
 
14
15
  from ophyd_async.core import (
16
+ DEFAULT_TIMEOUT,
17
+ NotConnected,
15
18
  ReadingValueCallback,
19
+ RuntimeSubsetEnum,
16
20
  SignalBackend,
17
21
  T,
18
22
  get_dtype,
19
23
  get_unique,
20
24
  wait_for_connection,
21
25
  )
22
- from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected
23
26
 
24
- from .common import LimitPair, Limits, common_meta, get_supported_values
27
+ from ._common import LimitPair, Limits, common_meta, get_supported_values
25
28
 
26
29
  # https://mdavidsaver.github.io/p4p/values.html
27
30
  specifier_to_dtype: Dict[str, Dtype] = {
@@ -39,6 +42,21 @@ specifier_to_dtype: Dict[str, Dtype] = {
39
42
  "s": "string",
40
43
  }
41
44
 
45
+ specifier_to_np_dtype: Dict[str, str] = {
46
+ "?": "<i2", # bool
47
+ "b": "|i1", # int8
48
+ "B": "|u1", # uint8
49
+ "h": "<i2", # int16
50
+ "H": "<u2", # uint16
51
+ "i": "<i4", # int32
52
+ "I": "<u4", # uint32
53
+ "l": "<i8", # int64
54
+ "L": "<u8", # uint64
55
+ "f": "<f4", # float32
56
+ "d": "<f8", # float64
57
+ "s": "|S40",
58
+ }
59
+
42
60
 
43
61
  def _data_key_from_value(
44
62
  source: str,
@@ -59,12 +77,35 @@ def _data_key_from_value(
59
77
  DataKey: A rich DataKey describing the DB record
60
78
  """
61
79
  shape = shape or []
62
- dtype = dtype or specifier_to_dtype[value.type().aspy("value")]
80
+ type_code = value.type().aspy("value")
81
+
82
+ dtype = dtype or specifier_to_dtype[type_code]
83
+
84
+ try:
85
+ if isinstance(type_code, tuple):
86
+ dtype_numpy = ""
87
+ if type_code[1] == "enum_t":
88
+ if dtype == "bool":
89
+ dtype_numpy = "<i2"
90
+ else:
91
+ for item in type_code[2]:
92
+ if item[0] == "choices":
93
+ dtype_numpy = specifier_to_np_dtype[item[1][1]]
94
+ elif not type_code.startswith("a"):
95
+ dtype_numpy = specifier_to_np_dtype[type_code]
96
+ else:
97
+ # Array type, use typecode of internal element
98
+ dtype_numpy = specifier_to_np_dtype[type_code[1]]
99
+ except KeyError:
100
+ # Case where we can't determine dtype string from value
101
+ dtype_numpy = ""
102
+
63
103
  display_data = getattr(value, "display", None)
64
104
 
65
105
  d = DataKey(
66
106
  source=source,
67
107
  dtype=dtype,
108
+ dtype_numpy=dtype_numpy,
68
109
  shape=shape,
69
110
  )
70
111
  if display_data is not None:
@@ -249,7 +290,7 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
249
290
  typ = get_unique(
250
291
  {k: type(v.get("value")) for k, v in values.items()}, "value types"
251
292
  )
252
- if "NTScalarArray" in typeid and typ == list:
293
+ if "NTScalarArray" in typeid and typ is list:
253
294
  # Waveform of strings, check we wanted this
254
295
  if datatype and datatype != Sequence[str]:
255
296
  raise TypeError(f"{pv} has type [str] not {datatype.__name__}")
@@ -287,6 +328,14 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
287
328
  return PvaEnumConverter(get_supported_values(pv, datatype, pv_choices))
288
329
  elif "NTScalar" in typeid:
289
330
  if (
331
+ typ is str
332
+ and inspect.isclass(datatype)
333
+ and issubclass(datatype, RuntimeSubsetEnum)
334
+ ):
335
+ return PvaEnumConverter(
336
+ get_supported_values(pv, datatype, datatype.choices)
337
+ )
338
+ elif (
290
339
  datatype
291
340
  and not issubclass(typ, datatype)
292
341
  and not (
@@ -14,26 +14,27 @@ from ophyd_async.core import (
14
14
  get_unique,
15
15
  )
16
16
 
17
- from ._epics_transport import EpicsTransport
17
+ from ._epics_transport import _EpicsTransport
18
18
 
19
- _default_epics_transport = EpicsTransport.ca
19
+ _default_epics_transport = _EpicsTransport.ca
20
20
 
21
21
 
22
- def _transport_pv(pv: str) -> Tuple[EpicsTransport, str]:
22
+ def _transport_pv(pv: str) -> Tuple[_EpicsTransport, str]:
23
23
  split = pv.split("://", 1)
24
24
  if len(split) > 1:
25
25
  # We got something like pva://mydevice, so use specified comms mode
26
26
  transport_str, pv = split
27
- transport = EpicsTransport[transport_str]
27
+ transport = _EpicsTransport[transport_str]
28
28
  else:
29
29
  # No comms mode specified, use the default
30
30
  transport = _default_epics_transport
31
31
  return transport, pv
32
32
 
33
33
 
34
- def _make_backend(
34
+ def _epics_signal_backend(
35
35
  datatype: Optional[Type[T]], read_pv: str, write_pv: str
36
36
  ) -> SignalBackend[T]:
37
+ """Create an epics signal backend."""
37
38
  r_transport, r_pv = _transport_pv(read_pv)
38
39
  w_transport, w_pv = _transport_pv(write_pv)
39
40
  transport = get_unique({read_pv: r_transport, write_pv: w_transport}, "transports")
@@ -54,7 +55,7 @@ def epics_signal_rw(
54
55
  write_pv:
55
56
  If given, use this PV to write to, otherwise use read_pv
56
57
  """
57
- backend = _make_backend(datatype, read_pv, write_pv or read_pv)
58
+ backend = _epics_signal_backend(datatype, read_pv, write_pv or read_pv)
58
59
  return SignalRW(backend, name=name)
59
60
 
60
61
 
@@ -85,7 +86,7 @@ def epics_signal_r(datatype: Type[T], read_pv: str, name: str = "") -> SignalR[T
85
86
  read_pv:
86
87
  The PV to read and monitor
87
88
  """
88
- backend = _make_backend(datatype, read_pv, read_pv)
89
+ backend = _epics_signal_backend(datatype, read_pv, read_pv)
89
90
  return SignalR(backend, name=name)
90
91
 
91
92
 
@@ -99,7 +100,7 @@ def epics_signal_w(datatype: Type[T], write_pv: str, name: str = "") -> SignalW[
99
100
  write_pv:
100
101
  The PV to write to
101
102
  """
102
- backend = _make_backend(datatype, write_pv, write_pv)
103
+ backend = _epics_signal_backend(datatype, write_pv, write_pv)
103
104
  return SignalW(backend, name=name)
104
105
 
105
106
 
@@ -111,5 +112,5 @@ def epics_signal_x(write_pv: str, name: str = "") -> SignalX:
111
112
  write_pv:
112
113
  The PV to write its initial value to on trigger
113
114
  """
114
- backend: SignalBackend = _make_backend(None, write_pv, write_pv)
115
+ backend: SignalBackend = _epics_signal_backend(None, write_pv, write_pv)
115
116
  return SignalX(backend, name=name)
File without changes
@@ -1,38 +1,57 @@
1
1
  from ._common_blocks import (
2
2
  CommonPandaBlocks,
3
3
  DataBlock,
4
+ EnableDisableOptions,
4
5
  PcapBlock,
6
+ PcompBlock,
7
+ PcompDirectionOptions,
5
8
  PulseBlock,
6
9
  SeqBlock,
7
10
  TimeUnits,
8
11
  )
9
12
  from ._hdf_panda import HDFPanda
13
+ from ._hdf_writer import PandaHDFWriter
10
14
  from ._panda_controller import PandaPcapController
11
15
  from ._table import (
16
+ DatasetTable,
17
+ PandaHdf5DatasetType,
12
18
  SeqTable,
13
19
  SeqTableRow,
14
20
  SeqTrigger,
15
21
  seq_table_from_arrays,
16
22
  seq_table_from_rows,
17
23
  )
18
- from ._trigger import StaticSeqTableTriggerLogic
24
+ from ._trigger import (
25
+ PcompInfo,
26
+ SeqTableInfo,
27
+ StaticPcompTriggerLogic,
28
+ StaticSeqTableTriggerLogic,
29
+ )
19
30
  from ._utils import phase_sorter
20
31
 
21
32
  __all__ = [
22
33
  "CommonPandaBlocks",
23
- "HDFPanda",
34
+ "DataBlock",
35
+ "EnableDisableOptions",
24
36
  "PcapBlock",
37
+ "PcompBlock",
38
+ "PcompDirectionOptions",
25
39
  "PulseBlock",
26
- "seq_table_from_arrays",
27
- "seq_table_from_rows",
28
40
  "SeqBlock",
41
+ "TimeUnits",
42
+ "HDFPanda",
43
+ "PandaHDFWriter",
44
+ "PandaPcapController",
45
+ "DatasetTable",
46
+ "PandaHdf5DatasetType",
29
47
  "SeqTable",
30
48
  "SeqTableRow",
31
49
  "SeqTrigger",
32
- "phase_sorter",
33
- "PandaPcapController",
34
- "TimeUnits",
35
- "DataBlock",
36
- "CommonPandABlocks",
50
+ "seq_table_from_arrays",
51
+ "seq_table_from_rows",
52
+ "PcompInfo",
53
+ "SeqTableInfo",
54
+ "StaticPcompTriggerLogic",
37
55
  "StaticSeqTableTriggerLogic",
56
+ "phase_sorter",
38
57
  ]