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.
Files changed (58) hide show
  1. ophyd_async/__init__.py +1 -4
  2. ophyd_async/_version.py +1 -1
  3. ophyd_async/core/__init__.py +12 -2
  4. ophyd_async/core/_providers.py +3 -1
  5. ophyd_async/core/detector.py +65 -38
  6. ophyd_async/core/device.py +8 -0
  7. ophyd_async/core/flyer.py +10 -19
  8. ophyd_async/core/signal.py +36 -17
  9. ophyd_async/core/signal_backend.py +5 -2
  10. ophyd_async/core/sim_signal_backend.py +28 -16
  11. ophyd_async/core/standard_readable.py +4 -2
  12. ophyd_async/core/utils.py +18 -1
  13. ophyd_async/epics/_backend/_aioca.py +13 -11
  14. ophyd_async/epics/_backend/_p4p.py +19 -16
  15. ophyd_async/epics/_backend/common.py +16 -11
  16. ophyd_async/epics/areadetector/__init__.py +4 -0
  17. ophyd_async/epics/areadetector/aravis.py +69 -0
  18. ophyd_async/epics/areadetector/controllers/aravis_controller.py +73 -0
  19. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +36 -24
  20. ophyd_async/epics/areadetector/drivers/aravis_driver.py +154 -0
  21. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +4 -4
  22. ophyd_async/epics/areadetector/pilatus.py +50 -0
  23. ophyd_async/epics/areadetector/writers/_hdffile.py +4 -4
  24. ophyd_async/epics/areadetector/writers/hdf_writer.py +6 -1
  25. ophyd_async/epics/demo/__init__.py +33 -3
  26. ophyd_async/epics/motion/motor.py +20 -14
  27. ophyd_async/epics/pvi/__init__.py +3 -0
  28. ophyd_async/epics/pvi/pvi.py +318 -0
  29. ophyd_async/epics/signal/signal.py +26 -9
  30. ophyd_async/panda/__init__.py +17 -6
  31. ophyd_async/panda/_common_blocks.py +49 -0
  32. ophyd_async/panda/_hdf_panda.py +48 -0
  33. ophyd_async/panda/{panda_controller.py → _panda_controller.py} +3 -7
  34. ophyd_async/panda/_trigger.py +39 -0
  35. ophyd_async/panda/writers/__init__.py +3 -0
  36. ophyd_async/panda/writers/_hdf_writer.py +220 -0
  37. ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
  38. ophyd_async/planstubs/__init__.py +5 -0
  39. ophyd_async/planstubs/prepare_trigger_and_dets.py +57 -0
  40. ophyd_async/protocols.py +73 -0
  41. ophyd_async/sim/__init__.py +11 -0
  42. ophyd_async/sim/demo/__init__.py +3 -0
  43. ophyd_async/sim/demo/sim_motor.py +116 -0
  44. ophyd_async/sim/pattern_generator.py +318 -0
  45. ophyd_async/sim/sim_pattern_detector_control.py +55 -0
  46. ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
  47. ophyd_async/sim/sim_pattern_generator.py +37 -0
  48. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/METADATA +19 -75
  49. ophyd_async-0.3a2.dist-info/RECORD +76 -0
  50. ophyd_async/epics/pvi.py +0 -70
  51. ophyd_async/panda/panda.py +0 -241
  52. ophyd_async-0.3a1.dist-info/RECORD +0 -56
  53. /ophyd_async/panda/{table.py → _table.py} +0 -0
  54. /ophyd_async/panda/{utils.py → _utils.py} +0 -0
  55. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/LICENSE +0 -0
  56. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/WHEEL +0 -0
  57. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/entry_points.txt +0 -0
  58. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,50 @@
1
+ from typing import Optional, Sequence
2
+
3
+ from bluesky.protocols import Hints
4
+
5
+ from ophyd_async.core import DirectoryProvider
6
+ from ophyd_async.core.detector import StandardDetector
7
+ from ophyd_async.core.signal import SignalR
8
+ from ophyd_async.epics.areadetector.controllers.pilatus_controller import (
9
+ PilatusController,
10
+ )
11
+ from ophyd_async.epics.areadetector.drivers.ad_base import ADBaseShapeProvider
12
+ from ophyd_async.epics.areadetector.drivers.pilatus_driver import PilatusDriver
13
+ from ophyd_async.epics.areadetector.writers.hdf_writer import HDFWriter
14
+ from ophyd_async.epics.areadetector.writers.nd_file_hdf import NDFileHDF
15
+
16
+
17
+ class PilatusDetector(StandardDetector):
18
+ """A Pilatus StandardDetector writing HDF files"""
19
+
20
+ _controller: PilatusController
21
+ _writer: HDFWriter
22
+
23
+ def __init__(
24
+ self,
25
+ name: str,
26
+ directory_provider: DirectoryProvider,
27
+ driver: PilatusDriver,
28
+ hdf: NDFileHDF,
29
+ config_sigs: Optional[Sequence[SignalR]] = None,
30
+ **scalar_sigs: str,
31
+ ):
32
+ self.drv = driver
33
+ self.hdf = hdf
34
+
35
+ super().__init__(
36
+ PilatusController(self.drv),
37
+ HDFWriter(
38
+ self.hdf,
39
+ directory_provider,
40
+ lambda: self.name,
41
+ ADBaseShapeProvider(self.drv),
42
+ **scalar_sigs,
43
+ ),
44
+ config_sigs=config_sigs or (self.drv.acquire_time,),
45
+ name=name,
46
+ )
47
+
48
+ @property
49
+ def hints(self) -> Hints:
50
+ return self._writer.hints
@@ -44,10 +44,10 @@ class _HDFFile:
44
44
  def stream_data(self, indices_written: int) -> Iterator[StreamDatum]:
45
45
  # Indices are relative to resource
46
46
  if indices_written > self._last_emitted:
47
- indices = dict(
48
- start=self._last_emitted,
49
- stop=indices_written,
50
- )
47
+ indices = {
48
+ "start": self._last_emitted,
49
+ "stop": indices_written,
50
+ }
51
51
  self._last_emitted = indices_written
52
52
  for bundle in self._bundles:
53
53
  yield bundle.compose_stream_datum(indices)
@@ -109,12 +109,17 @@ class HDFWriter(DetectorWriter):
109
109
  await self.hdf.flush_now.set(True)
110
110
  if indices_written:
111
111
  if not self._file:
112
+ path = Path(await self.hdf.full_file_name.get_value())
112
113
  self._file = _HDFFile(
113
114
  self._directory_provider(),
114
115
  # See https://github.com/bluesky/ophyd-async/issues/122
115
- Path(await self.hdf.full_file_name.get_value()),
116
+ path,
116
117
  self._datasets,
117
118
  )
119
+ # stream resource says "here is a dataset",
120
+ # stream datum says "here are N frames in that stream resource",
121
+ # you get one stream resource and many stream datums per scan
122
+
118
123
  for doc in self._file.stream_resources():
119
124
  yield "stream_resource", doc
120
125
  for doc in self._file.stream_data(indices_written):
@@ -14,7 +14,13 @@ from typing import Callable, List, Optional
14
14
  import numpy as np
15
15
  from bluesky.protocols import Movable, Stoppable
16
16
 
17
- from ophyd_async.core import AsyncStatus, Device, StandardReadable, observe_value
17
+ from ophyd_async.core import (
18
+ AsyncStatus,
19
+ Device,
20
+ DeviceVector,
21
+ StandardReadable,
22
+ observe_value,
23
+ )
18
24
 
19
25
  from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
20
26
 
@@ -43,6 +49,19 @@ class Sensor(StandardReadable):
43
49
  super().__init__(name=name)
44
50
 
45
51
 
52
+ class SensorGroup(StandardReadable):
53
+ def __init__(self, prefix: str, name: str = "", sensor_count: int = 3) -> None:
54
+ self.sensors = DeviceVector(
55
+ {i: Sensor(f"{prefix}{i}:") for i in range(1, sensor_count + 1)}
56
+ )
57
+
58
+ # Makes read() produce the values of all sensors
59
+ self.set_readable_signals(
60
+ read=[sensor.value for sensor in self.sensors.values()],
61
+ )
62
+ super().__init__(name)
63
+
64
+
46
65
  class Mover(StandardReadable, Movable, Stoppable):
47
66
  """A demo movable that moves based on velocity"""
48
67
 
@@ -135,11 +154,22 @@ def start_ioc_subprocess() -> str:
135
154
  pv_prefix = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + ":"
136
155
  here = Path(__file__).absolute().parent
137
156
  args = [sys.executable, "-m", "epicscorelibs.ioc"]
157
+
158
+ # Create standalone sensor
138
159
  args += ["-m", f"P={pv_prefix}"]
139
160
  args += ["-d", str(here / "sensor.db")]
140
- for suff in "XY":
141
- args += ["-m", f"P={pv_prefix}{suff}:"]
161
+
162
+ # Create sensor group
163
+ for suffix in ["1", "2", "3"]:
164
+ args += ["-m", f"P={pv_prefix}{suffix}:"]
165
+ args += ["-d", str(here / "sensor.db")]
166
+
167
+ # Create X and Y motors
168
+ for suffix in ["X", "Y"]:
169
+ args += ["-m", f"P={pv_prefix}{suffix}:"]
142
170
  args += ["-d", str(here / "mover.db")]
171
+
172
+ # Start IOC
143
173
  process = subprocess.Popen(
144
174
  args,
145
175
  stdin=subprocess.PIPE,
@@ -14,33 +14,39 @@ class Motor(StandardReadable, Movable, Stoppable):
14
14
 
15
15
  def __init__(self, prefix: str, name="") -> None:
16
16
  # Define some signals
17
- self.setpoint = epics_signal_rw(float, prefix + ".VAL")
18
- self.readback = epics_signal_r(float, prefix + ".RBV")
17
+ self.user_setpoint = epics_signal_rw(float, prefix + ".VAL")
18
+ self.user_readback = epics_signal_r(float, prefix + ".RBV")
19
19
  self.velocity = epics_signal_rw(float, prefix + ".VELO")
20
- self.units = epics_signal_r(str, prefix + ".EGU")
20
+ self.max_velocity = epics_signal_r(float, prefix + ".VMAX")
21
+ self.acceleration_time = epics_signal_rw(float, prefix + ".ACCL")
22
+ self.motor_egu = epics_signal_r(str, prefix + ".EGU")
21
23
  self.precision = epics_signal_r(int, prefix + ".PREC")
22
- # Signals that collide with standard methods should have a trailing underscore
23
- self.stop_ = epics_signal_x(prefix + ".STOP")
24
+ self.deadband = epics_signal_r(float, prefix + ".RDBD")
25
+ self.motor_done_move = epics_signal_r(float, prefix + ".DMOV")
26
+ self.low_limit_travel = epics_signal_rw(int, prefix + ".LLM")
27
+ self.high_limit_travel = epics_signal_rw(int, prefix + ".HLM")
28
+
29
+ self.motor_stop = epics_signal_x(prefix + ".STOP")
24
30
  # Whether set() should complete successfully or not
25
31
  self._set_success = True
26
32
  # Set name and signals for read() and read_configuration()
27
33
  self.set_readable_signals(
28
- read=[self.readback],
29
- config=[self.velocity, self.units],
34
+ read=[self.user_readback],
35
+ config=[self.velocity, self.motor_egu],
30
36
  )
31
37
  super().__init__(name=name)
32
38
 
33
39
  def set_name(self, name: str):
34
40
  super().set_name(name)
35
41
  # Readback should be named the same as its parent in read()
36
- self.readback.set_name(name)
42
+ self.user_readback.set_name(name)
37
43
 
38
44
  async def _move(self, new_position: float, watchers: List[Callable] = []):
39
45
  self._set_success = True
40
46
  start = time.monotonic()
41
47
  old_position, units, precision = await asyncio.gather(
42
- self.setpoint.get_value(),
43
- self.units.get_value(),
48
+ self.user_setpoint.get_value(),
49
+ self.motor_egu.get_value(),
44
50
  self.precision.get_value(),
45
51
  )
46
52
 
@@ -56,11 +62,11 @@ class Motor(StandardReadable, Movable, Stoppable):
56
62
  time_elapsed=time.monotonic() - start,
57
63
  )
58
64
 
59
- self.readback.subscribe_value(update_watchers)
65
+ self.user_readback.subscribe_value(update_watchers)
60
66
  try:
61
- await self.setpoint.set(new_position)
67
+ await self.user_setpoint.set(new_position)
62
68
  finally:
63
- self.readback.clear_sub(update_watchers)
69
+ self.user_readback.clear_sub(update_watchers)
64
70
  if not self._set_success:
65
71
  raise RuntimeError("Motor was stopped")
66
72
 
@@ -81,5 +87,5 @@ class Motor(StandardReadable, Movable, Stoppable):
81
87
  self._set_success = success
82
88
  # Put with completion will never complete as we are waiting for completion on
83
89
  # the move above, so need to pass wait=False
84
- status = self.stop_.trigger(wait=False)
90
+ status = self.motor_stop.trigger(wait=False)
85
91
  await status
@@ -0,0 +1,3 @@
1
+ from .pvi import PVIEntry, create_children_from_annotations, fill_pvi_entries
2
+
3
+ __all__ = ["PVIEntry", "fill_pvi_entries", "create_children_from_annotations"]
@@ -0,0 +1,318 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+ from inspect import isclass
4
+ from typing import (
5
+ Any,
6
+ Callable,
7
+ Dict,
8
+ FrozenSet,
9
+ Literal,
10
+ Optional,
11
+ Tuple,
12
+ Type,
13
+ TypeVar,
14
+ Union,
15
+ get_args,
16
+ get_origin,
17
+ get_type_hints,
18
+ )
19
+
20
+ from ophyd_async.core import Device, DeviceVector, SimSignalBackend
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 (
25
+ epics_signal_r,
26
+ epics_signal_rw,
27
+ epics_signal_w,
28
+ epics_signal_x,
29
+ )
30
+
31
+ T = TypeVar("T")
32
+ Access = FrozenSet[
33
+ Union[Literal["r"], Literal["w"], Literal["rw"], Literal["x"], Literal["d"]]
34
+ ]
35
+
36
+
37
+ def _strip_number_from_string(string: str) -> Tuple[str, Optional[int]]:
38
+ match = re.match(r"(.*?)(\d*)$", string)
39
+ assert match
40
+
41
+ name = match.group(1)
42
+ number = match.group(2) or None
43
+ if number:
44
+ number = int(number)
45
+ return name, number
46
+
47
+
48
+ def _split_subscript(tp: T) -> Union[Tuple[Any, Tuple[Any]], Tuple[T, None]]:
49
+ """Split a subscripted type into the its origin and args.
50
+
51
+ If `tp` is not a subscripted type, then just return the type and None as args.
52
+
53
+ """
54
+ if get_origin(tp) is not None:
55
+ return get_origin(tp), get_args(tp)
56
+
57
+ return tp, None
58
+
59
+
60
+ def _strip_union(field: Union[Union[T], T]) -> Tuple[T, bool]:
61
+ if get_origin(field) is Union:
62
+ args = get_args(field)
63
+ is_optional = type(None) in args
64
+ for arg in args:
65
+ if arg is not type(None):
66
+ return arg, is_optional
67
+ return field, False
68
+
69
+
70
+ def _strip_device_vector(field: Union[Type[Device]]) -> Tuple[bool, Type[Device]]:
71
+ if get_origin(field) is DeviceVector:
72
+ return True, get_args(field)[0]
73
+ return False, field
74
+
75
+
76
+ @dataclass
77
+ class PVIEntry:
78
+ """
79
+ A dataclass to represent a single entry in the PVI table.
80
+ This could either be a signal or a sub-table.
81
+ """
82
+
83
+ sub_entries: Dict[str, Union[Dict[int, "PVIEntry"], "PVIEntry"]]
84
+ pvi_pv: Optional[str] = None
85
+ device: Optional[Device] = None
86
+ common_device_type: Optional[Type[Device]] = None
87
+
88
+
89
+ def _verify_common_blocks(entry: PVIEntry, common_device: Type[Device]):
90
+ if not entry.sub_entries:
91
+ return
92
+ common_sub_devices = get_type_hints(common_device)
93
+ for sub_name, sub_device in common_sub_devices.items():
94
+ if sub_name in ("_name", "parent"):
95
+ continue
96
+ assert entry.sub_entries
97
+ device_t, is_optional = _strip_union(sub_device)
98
+ if sub_name not in entry.sub_entries and not is_optional:
99
+ raise RuntimeError(
100
+ f"sub device `{sub_name}:{type(sub_device)}` " "was not provided by pvi"
101
+ )
102
+ if isinstance(entry.sub_entries[sub_name], dict):
103
+ for sub_sub_entry in entry.sub_entries[sub_name].values(): # type: ignore
104
+ _verify_common_blocks(sub_sub_entry, sub_device) # type: ignore
105
+ else:
106
+ _verify_common_blocks(
107
+ entry.sub_entries[sub_name],
108
+ sub_device, # type: ignore
109
+ )
110
+
111
+
112
+ _pvi_mapping: Dict[FrozenSet[str], Callable[..., Signal]] = {
113
+ frozenset({"r", "w"}): lambda dtype, read_pv, write_pv: epics_signal_rw(
114
+ dtype, "pva://" + read_pv, "pva://" + write_pv
115
+ ),
116
+ frozenset({"rw"}): lambda dtype, read_write_pv: epics_signal_rw(
117
+ dtype, "pva://" + read_write_pv, write_pv="pva://" + read_write_pv
118
+ ),
119
+ frozenset({"r"}): lambda dtype, read_pv: epics_signal_r(dtype, "pva://" + read_pv),
120
+ frozenset({"w"}): lambda dtype, write_pv: epics_signal_w(
121
+ dtype, "pva://" + write_pv
122
+ ),
123
+ frozenset({"x"}): lambda _, write_pv: epics_signal_x("pva://" + write_pv),
124
+ }
125
+
126
+
127
+ def _parse_type(
128
+ is_pvi_table: bool,
129
+ number_suffix: Optional[int],
130
+ common_device_type: Optional[Type[Device]],
131
+ ):
132
+ if common_device_type:
133
+ # pre-defined type
134
+ device_cls, _ = _strip_union(common_device_type)
135
+ is_device_vector, device_cls = _strip_device_vector(device_cls)
136
+ device_cls, device_args = _split_subscript(device_cls)
137
+ assert issubclass(device_cls, Device)
138
+
139
+ is_signal = issubclass(device_cls, Signal)
140
+ signal_dtype = device_args[0] if device_args is not None else None
141
+
142
+ elif is_pvi_table:
143
+ # is a block, we can make it a DeviceVector if it ends in a number
144
+ is_device_vector = number_suffix is not None
145
+ is_signal = False
146
+ signal_dtype = None
147
+ device_cls = Device
148
+ else:
149
+ # is a signal, signals aren't stored in DeviceVectors unless
150
+ # they're defined as such in the common_device_type
151
+ is_device_vector = False
152
+ is_signal = True
153
+ signal_dtype = None
154
+ device_cls = Signal
155
+
156
+ return is_device_vector, is_signal, signal_dtype, device_cls
157
+
158
+
159
+ def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None):
160
+ device_t = stripped_type or type(device)
161
+ sub_devices = (
162
+ (field, field_type)
163
+ for field, field_type in get_type_hints(device_t).items()
164
+ if field not in ("_name", "parent")
165
+ )
166
+
167
+ for device_name, device_cls in sub_devices:
168
+ device_cls, _ = _strip_union(device_cls)
169
+ is_device_vector, device_cls = _strip_device_vector(device_cls)
170
+ device_cls, device_args = _split_subscript(device_cls)
171
+ assert issubclass(device_cls, Device)
172
+
173
+ is_signal = issubclass(device_cls, Signal)
174
+ signal_dtype = device_args[0] if device_args is not None else None
175
+
176
+ if is_device_vector:
177
+ if is_signal:
178
+ sub_device_1 = device_cls(SimSignalBackend(signal_dtype))
179
+ sub_device_2 = device_cls(SimSignalBackend(signal_dtype))
180
+ sub_device = DeviceVector({1: sub_device_1, 2: sub_device_2})
181
+ else:
182
+ sub_device = DeviceVector({1: device_cls(), 2: device_cls()})
183
+
184
+ for sub_device_in_vector in sub_device.values():
185
+ _sim_common_blocks(sub_device_in_vector, stripped_type=device_cls)
186
+
187
+ for value in sub_device.values():
188
+ value.parent = sub_device
189
+ else:
190
+ if is_signal:
191
+ sub_device = device_cls(SimSignalBackend(signal_dtype))
192
+ else:
193
+ sub_device = getattr(device, device_name, device_cls())
194
+ _sim_common_blocks(sub_device, stripped_type=device_cls)
195
+
196
+ setattr(device, device_name, sub_device)
197
+ sub_device.parent = device
198
+
199
+
200
+ async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT):
201
+ if not entry.pvi_pv or not entry.pvi_pv.endswith(":PVI"):
202
+ raise RuntimeError("Top level entry must be a pvi table")
203
+
204
+ pvi_table_signal_backend: PvaSignalBackend = PvaSignalBackend(
205
+ None, entry.pvi_pv, entry.pvi_pv
206
+ )
207
+ await pvi_table_signal_backend.connect(
208
+ timeout=timeout
209
+ ) # create table signal backend
210
+
211
+ pva_table = (await pvi_table_signal_backend.get_value())["pvi"]
212
+ common_device_type_hints = (
213
+ get_type_hints(entry.common_device_type) if entry.common_device_type else {}
214
+ )
215
+
216
+ for sub_name, pva_entries in pva_table.items():
217
+ pvs = list(pva_entries.values())
218
+ is_pvi_table = len(pvs) == 1 and pvs[0].endswith(":PVI")
219
+ sub_name_split, sub_number_split = _strip_number_from_string(sub_name)
220
+ is_device_vector, is_signal, signal_dtype, device_type = _parse_type(
221
+ is_pvi_table,
222
+ sub_number_split,
223
+ common_device_type_hints.get(sub_name_split),
224
+ )
225
+ if is_signal:
226
+ device = _pvi_mapping[frozenset(pva_entries.keys())](signal_dtype, *pvs)
227
+ else:
228
+ device = getattr(entry.device, sub_name, device_type())
229
+
230
+ sub_entry = PVIEntry(
231
+ device=device, common_device_type=device_type, sub_entries={}
232
+ )
233
+
234
+ if is_device_vector:
235
+ # If device vector then we store sub_name -> {sub_number -> sub_entry}
236
+ # and aggregate into `DeviceVector` in `_set_device_attributes`
237
+ sub_number_split = 1 if sub_number_split is None else sub_number_split
238
+ if sub_name_split not in entry.sub_entries:
239
+ entry.sub_entries[sub_name_split] = {}
240
+ entry.sub_entries[sub_name_split][sub_number_split] = sub_entry # type: ignore
241
+ else:
242
+ entry.sub_entries[sub_name] = sub_entry
243
+
244
+ if is_pvi_table:
245
+ sub_entry.pvi_pv = pvs[0]
246
+ await _get_pvi_entries(sub_entry)
247
+
248
+ if entry.common_device_type:
249
+ _verify_common_blocks(entry, entry.common_device_type)
250
+
251
+
252
+ def _set_device_attributes(entry: PVIEntry):
253
+ for sub_name, sub_entry in entry.sub_entries.items():
254
+ if isinstance(sub_entry, dict):
255
+ sub_device = DeviceVector() # type: ignore
256
+ for key, device_vector_sub_entry in sub_entry.items():
257
+ sub_device[key] = device_vector_sub_entry.device
258
+ if device_vector_sub_entry.pvi_pv:
259
+ _set_device_attributes(device_vector_sub_entry)
260
+ # Set the device vector entry to have the device vector as a parent
261
+ device_vector_sub_entry.device.parent = sub_device # type: ignore
262
+ else:
263
+ sub_device = sub_entry.device # type: ignore
264
+ if sub_entry.pvi_pv:
265
+ _set_device_attributes(sub_entry)
266
+
267
+ sub_device.parent = entry.device
268
+ setattr(entry.device, sub_name, sub_device)
269
+
270
+
271
+ async def fill_pvi_entries(
272
+ device: Device, root_pv: str, timeout=DEFAULT_TIMEOUT, sim=False
273
+ ):
274
+ """
275
+ Fills a ``device`` with signals from a the ``root_pvi:PVI`` table.
276
+
277
+ If the device names match with parent devices of ``device`` then types are used.
278
+ """
279
+ if sim:
280
+ # set up sim signals for the common annotations
281
+ _sim_common_blocks(device)
282
+ else:
283
+ # check the pvi table for devices and fill the device with them
284
+ root_entry = PVIEntry(
285
+ pvi_pv=root_pv,
286
+ device=device,
287
+ common_device_type=type(device),
288
+ sub_entries={},
289
+ )
290
+ await _get_pvi_entries(root_entry, timeout=timeout)
291
+ _set_device_attributes(root_entry)
292
+
293
+ # We call set name now the parent field has been set in all of the
294
+ # introspect-initialized devices. This will recursively set the names.
295
+ device.set_name(device.name)
296
+
297
+
298
+ def create_children_from_annotations(
299
+ device: Device, included_optional_fields: Tuple[str, ...] = ()
300
+ ):
301
+ """For intializing blocks at __init__ of ``device``."""
302
+ for name, device_type in get_type_hints(type(device)).items():
303
+ if name in ("_name", "parent"):
304
+ continue
305
+ device_type, is_optional = _strip_union(device_type)
306
+ if is_optional and name not in included_optional_fields:
307
+ continue
308
+ is_device_vector, device_type = _strip_device_vector(device_type)
309
+ if (
310
+ is_device_vector
311
+ or ((origin := get_origin(device_type)) and issubclass(origin, Signal))
312
+ or (isclass(device_type) and issubclass(device_type, Signal))
313
+ ):
314
+ continue
315
+
316
+ sub_device = device_type()
317
+ setattr(device, name, sub_device)
318
+ create_children_from_annotations(sub_device)
@@ -41,7 +41,7 @@ def _make_backend(
41
41
 
42
42
 
43
43
  def epics_signal_rw(
44
- datatype: Type[T], read_pv: str, write_pv: Optional[str] = None
44
+ datatype: Type[T], read_pv: str, write_pv: Optional[str] = None, name: str = ""
45
45
  ) -> SignalRW[T]:
46
46
  """Create a `SignalRW` backed by 1 or 2 EPICS PVs
47
47
 
@@ -55,24 +55,41 @@ def epics_signal_rw(
55
55
  If given, use this PV to write to, otherwise use read_pv
56
56
  """
57
57
  backend = _make_backend(datatype, read_pv, write_pv or read_pv)
58
- return SignalRW(backend)
58
+ return SignalRW(backend, name=name)
59
59
 
60
60
 
61
- def epics_signal_r(datatype: Type[T], read_pv: str) -> SignalR[T]:
62
- """Create a `SignalR` backed by 1 EPICS PV
61
+ def epics_signal_rw_rbv(
62
+ datatype: Type[T], write_pv: str, read_suffix: str = "_RBV", name: str = ""
63
+ ) -> SignalRW[T]:
64
+ """Create a `SignalRW` backed by 1 or 2 EPICS PVs, with a suffix on the readback pv
63
65
 
64
66
  Parameters
65
67
  ----------
66
68
  datatype:
67
69
  Check that the PV is of this type
70
+ write_pv:
71
+ The PV to write to
72
+ read_suffix:
73
+ Append this suffix to the write pv to create the readback pv
74
+ """
75
+ return epics_signal_rw(datatype, f"{write_pv}{read_suffix}", write_pv, name)
76
+
77
+
78
+ def epics_signal_r(datatype: Type[T], read_pv: str, name: str = "") -> SignalR[T]:
79
+ """Create a `SignalR` backed by 1 EPICS PV
80
+
81
+ Parameters
82
+ ---------
83
+ datatype
84
+ Check that the PV is of this type
68
85
  read_pv:
69
86
  The PV to read and monitor
70
87
  """
71
88
  backend = _make_backend(datatype, read_pv, read_pv)
72
- return SignalR(backend)
89
+ return SignalR(backend, name=name)
73
90
 
74
91
 
75
- def epics_signal_w(datatype: Type[T], write_pv: str) -> SignalW[T]:
92
+ def epics_signal_w(datatype: Type[T], write_pv: str, name: str = "") -> SignalW[T]:
76
93
  """Create a `SignalW` backed by 1 EPICS PVs
77
94
 
78
95
  Parameters
@@ -83,10 +100,10 @@ def epics_signal_w(datatype: Type[T], write_pv: str) -> SignalW[T]:
83
100
  The PV to write to
84
101
  """
85
102
  backend = _make_backend(datatype, write_pv, write_pv)
86
- return SignalW(backend)
103
+ return SignalW(backend, name=name)
87
104
 
88
105
 
89
- def epics_signal_x(write_pv: str) -> SignalX:
106
+ def epics_signal_x(write_pv: str, name: str = "") -> SignalX:
90
107
  """Create a `SignalX` backed by 1 EPICS PVs
91
108
 
92
109
  Parameters
@@ -95,4 +112,4 @@ def epics_signal_x(write_pv: str) -> SignalX:
95
112
  The PV to write its initial value to on trigger
96
113
  """
97
114
  backend: SignalBackend = _make_backend(None, write_pv, write_pv)
98
- return SignalX(backend)
115
+ return SignalX(backend, name=name)
@@ -1,19 +1,27 @@
1
- from .panda import PandA, PcapBlock, PulseBlock, PVIEntry, SeqBlock, SeqTable
2
- from .panda_controller import PandaPcapController
3
- from .table import (
1
+ from ._common_blocks import (
2
+ CommonPandaBlocks,
3
+ DataBlock,
4
+ PcapBlock,
5
+ PulseBlock,
6
+ SeqBlock,
7
+ TimeUnits,
8
+ )
9
+ from ._hdf_panda import HDFPanda
10
+ from ._panda_controller import PandaPcapController
11
+ from ._table import (
4
12
  SeqTable,
5
13
  SeqTableRow,
6
14
  SeqTrigger,
7
15
  seq_table_from_arrays,
8
16
  seq_table_from_rows,
9
17
  )
10
- from .utils import phase_sorter
18
+ from ._utils import phase_sorter
11
19
 
12
20
  __all__ = [
13
- "PandA",
21
+ "CommonPandaBlocks",
22
+ "HDFPanda",
14
23
  "PcapBlock",
15
24
  "PulseBlock",
16
- "PVIEntry",
17
25
  "seq_table_from_arrays",
18
26
  "seq_table_from_rows",
19
27
  "SeqBlock",
@@ -22,4 +30,7 @@ __all__ = [
22
30
  "SeqTrigger",
23
31
  "phase_sorter",
24
32
  "PandaPcapController",
33
+ "TimeUnits",
34
+ "DataBlock",
35
+ "CommonPandABlocks",
25
36
  ]