ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.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 (157) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +102 -74
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +158 -153
  8. ophyd_async/core/_device.py +143 -115
  9. ophyd_async/core/_device_filler.py +82 -9
  10. ophyd_async/core/_flyer.py +16 -7
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +74 -58
  17. ophyd_async/core/_settings.py +113 -0
  18. ophyd_async/core/_signal.py +304 -174
  19. ophyd_async/core/_signal_backend.py +60 -14
  20. ophyd_async/core/_soft_signal_backend.py +18 -12
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +54 -17
  23. ophyd_async/core/_utils.py +101 -52
  24. ophyd_async/core/_yaml_settings.py +66 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/__init__.py +9 -0
  27. ophyd_async/epics/adandor/_andor.py +45 -0
  28. ophyd_async/epics/adandor/_andor_controller.py +51 -0
  29. ophyd_async/epics/adandor/_andor_io.py +34 -0
  30. ophyd_async/epics/adaravis/__init__.py +8 -1
  31. ophyd_async/epics/adaravis/_aravis.py +23 -41
  32. ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
  33. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  34. ophyd_async/epics/adcore/__init__.py +36 -14
  35. ophyd_async/epics/adcore/_core_detector.py +81 -0
  36. ophyd_async/epics/adcore/_core_io.py +145 -95
  37. ophyd_async/epics/adcore/_core_logic.py +179 -88
  38. ophyd_async/epics/adcore/_core_writer.py +223 -0
  39. ophyd_async/epics/adcore/_hdf_writer.py +51 -92
  40. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  41. ophyd_async/epics/adcore/_single_trigger.py +6 -5
  42. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  43. ophyd_async/epics/adcore/_utils.py +3 -2
  44. ophyd_async/epics/adkinetix/__init__.py +2 -1
  45. ophyd_async/epics/adkinetix/_kinetix.py +32 -27
  46. ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
  47. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  48. ophyd_async/epics/adpilatus/__init__.py +7 -2
  49. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  50. ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
  51. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  52. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  53. ophyd_async/epics/adsimdetector/_sim.py +22 -16
  54. ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
  55. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  56. ophyd_async/epics/advimba/__init__.py +10 -1
  57. ophyd_async/epics/advimba/_vimba.py +26 -25
  58. ophyd_async/epics/advimba/_vimba_controller.py +12 -24
  59. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  60. ophyd_async/epics/core/_aioca.py +66 -30
  61. ophyd_async/epics/core/_epics_connector.py +4 -0
  62. ophyd_async/epics/core/_epics_device.py +2 -0
  63. ophyd_async/epics/core/_p4p.py +50 -18
  64. ophyd_async/epics/core/_pvi_connector.py +65 -8
  65. ophyd_async/epics/core/_signal.py +51 -51
  66. ophyd_async/epics/core/_util.py +5 -5
  67. ophyd_async/epics/demo/__init__.py +11 -49
  68. ophyd_async/epics/demo/__main__.py +31 -0
  69. ophyd_async/epics/demo/_ioc.py +32 -0
  70. ophyd_async/epics/demo/_motor.py +82 -0
  71. ophyd_async/epics/demo/_point_detector.py +42 -0
  72. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  73. ophyd_async/epics/demo/_stage.py +15 -0
  74. ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
  75. ophyd_async/epics/demo/point_detector.db +59 -0
  76. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  77. ophyd_async/epics/eiger/_eiger.py +1 -3
  78. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  79. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  80. ophyd_async/epics/eiger/_odin_io.py +1 -2
  81. ophyd_async/epics/motor.py +83 -38
  82. ophyd_async/epics/signal.py +4 -1
  83. ophyd_async/epics/testing/__init__.py +14 -14
  84. ophyd_async/epics/testing/_example_ioc.py +68 -73
  85. ophyd_async/epics/testing/_utils.py +19 -44
  86. ophyd_async/epics/testing/test_records.db +16 -0
  87. ophyd_async/epics/testing/test_records_pva.db +17 -16
  88. ophyd_async/fastcs/__init__.py +1 -0
  89. ophyd_async/fastcs/core.py +6 -0
  90. ophyd_async/fastcs/odin/__init__.py +1 -0
  91. ophyd_async/fastcs/panda/__init__.py +8 -8
  92. ophyd_async/fastcs/panda/_block.py +29 -9
  93. ophyd_async/fastcs/panda/_control.py +12 -2
  94. ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
  95. ophyd_async/fastcs/panda/_table.py +13 -7
  96. ophyd_async/fastcs/panda/_trigger.py +23 -9
  97. ophyd_async/fastcs/panda/_writer.py +27 -30
  98. ophyd_async/plan_stubs/__init__.py +16 -0
  99. ophyd_async/plan_stubs/_ensure_connected.py +12 -17
  100. ophyd_async/plan_stubs/_fly.py +3 -5
  101. ophyd_async/plan_stubs/_nd_attributes.py +9 -5
  102. ophyd_async/plan_stubs/_panda.py +14 -0
  103. ophyd_async/plan_stubs/_settings.py +152 -0
  104. ophyd_async/plan_stubs/_utils.py +3 -0
  105. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  106. ophyd_async/sim/__init__.py +29 -0
  107. ophyd_async/sim/__main__.py +43 -0
  108. ophyd_async/sim/_blob_detector.py +33 -0
  109. ophyd_async/sim/_blob_detector_controller.py +48 -0
  110. ophyd_async/sim/_blob_detector_writer.py +105 -0
  111. ophyd_async/sim/_mirror_horizontal.py +46 -0
  112. ophyd_async/sim/_mirror_vertical.py +74 -0
  113. ophyd_async/sim/_motor.py +233 -0
  114. ophyd_async/sim/_pattern_generator.py +124 -0
  115. ophyd_async/sim/_point_detector.py +86 -0
  116. ophyd_async/sim/_stage.py +19 -0
  117. ophyd_async/tango/__init__.py +1 -0
  118. ophyd_async/tango/core/__init__.py +6 -1
  119. ophyd_async/tango/core/_base_device.py +41 -33
  120. ophyd_async/tango/core/_converters.py +81 -0
  121. ophyd_async/tango/core/_signal.py +21 -33
  122. ophyd_async/tango/core/_tango_readable.py +2 -19
  123. ophyd_async/tango/core/_tango_transport.py +148 -74
  124. ophyd_async/tango/core/_utils.py +47 -0
  125. ophyd_async/tango/demo/_counter.py +2 -0
  126. ophyd_async/tango/demo/_detector.py +2 -0
  127. ophyd_async/tango/demo/_mover.py +10 -6
  128. ophyd_async/tango/demo/_tango/_servers.py +4 -0
  129. ophyd_async/tango/testing/__init__.py +6 -0
  130. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  131. ophyd_async/testing/__init__.py +48 -7
  132. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  133. ophyd_async/testing/_assert.py +200 -96
  134. ophyd_async/testing/_mock_signal_utils.py +59 -73
  135. ophyd_async/testing/_one_of_everything.py +146 -0
  136. ophyd_async/testing/_single_derived.py +87 -0
  137. ophyd_async/testing/_utils.py +3 -0
  138. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
  139. ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
  140. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
  141. ophyd_async/core/_device_save_loader.py +0 -274
  142. ophyd_async/epics/demo/_mover.py +0 -95
  143. ophyd_async/epics/demo/_sensor.py +0 -37
  144. ophyd_async/epics/demo/sensor.db +0 -19
  145. ophyd_async/fastcs/panda/_utils.py +0 -16
  146. ophyd_async/sim/demo/__init__.py +0 -19
  147. ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
  148. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
  149. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
  150. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
  151. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
  152. ophyd_async/sim/demo/_sim_motor.py +0 -107
  153. ophyd_async/sim/testing/__init__.py +0 -0
  154. ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
  155. ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
  156. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
  157. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,8 @@
1
+ """Support for EPICS motor record.
2
+
3
+ https://github.com/epics-modules/motor
4
+ """
5
+
1
6
  import asyncio
2
7
 
3
8
  from bluesky.protocols import (
@@ -5,7 +10,9 @@ from bluesky.protocols import (
5
10
  Locatable,
6
11
  Location,
7
12
  Preparable,
13
+ Reading,
8
14
  Stoppable,
15
+ Subscribable,
9
16
  )
10
17
  from pydantic import BaseModel, Field
11
18
 
@@ -14,7 +21,9 @@ from ophyd_async.core import (
14
21
  DEFAULT_TIMEOUT,
15
22
  AsyncStatus,
16
23
  CalculatableTimeout,
24
+ Callback,
17
25
  StandardReadable,
26
+ StrictEnum,
18
27
  WatchableAsyncStatus,
19
28
  WatcherUpdate,
20
29
  observe_value,
@@ -22,47 +31,61 @@ from ophyd_async.core import (
22
31
  from ophyd_async.core import StandardReadableFormat as Format
23
32
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
24
33
 
34
+ __all__ = ["MotorLimitsException", "FlyMotorInfo", "Motor"]
25
35
 
26
- class MotorLimitsException(Exception):
27
- pass
28
36
 
37
+ class MotorLimitsException(Exception):
38
+ """Exception for invalid motor limits."""
29
39
 
30
- class InvalidFlyMotorException(Exception):
31
40
  pass
32
41
 
33
42
 
34
- DEFAULT_MOTOR_FLY_TIMEOUT = 60
35
- DEFAULT_WATCHER_UPDATE_FREQUENCY = 0.2
36
-
37
-
38
43
  class FlyMotorInfo(BaseModel):
39
- """Minimal set of information required to fly a motor:"""
44
+ """Minimal set of information required to fly a motor."""
40
45
 
41
- #: Absolute position of the motor once it finishes accelerating to desired
42
- #: velocity, in motor EGUs
43
46
  start_position: float = Field(frozen=True)
47
+ """Absolute position of the motor once it finishes accelerating to desired
48
+ velocity, in motor EGUs"""
44
49
 
45
- #: Absolute position of the motor once it begins decelerating from desired
46
- #: velocity, in EGUs
47
50
  end_position: float = Field(frozen=True)
51
+ """Absolute position of the motor once it begins decelerating from desired
52
+ velocity, in EGUs"""
48
53
 
49
- #: Time taken for the motor to get from start_position to end_position, excluding
50
- #: run-up and run-down, in seconds.
51
54
  time_for_move: float = Field(frozen=True, gt=0)
55
+ """Time taken for the motor to get from start_position to end_position, excluding
56
+ run-up and run-down, in seconds."""
52
57
 
53
- #: Maximum time for the complete motor move, including run up and run down.
54
- #: Defaults to `time_for_move` + run up and run down times + 10s.
55
58
  timeout: CalculatableTimeout = Field(frozen=True, default=CALCULATE_TIMEOUT)
59
+ """Maximum time for the complete motor move, including run up and run down.
60
+ Defaults to `time_for_move` + run up and run down times + 10s."""
61
+
62
+
63
+ class OffsetMode(StrictEnum):
64
+ VARIABLE = "Variable"
65
+ FROZEN = "Frozen"
56
66
 
57
67
 
58
- class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
59
- """Device that moves a motor record"""
68
+ class UseSetMode(StrictEnum):
69
+ USE = "Use"
70
+ SET = "Set"
71
+
72
+
73
+ class Motor(
74
+ StandardReadable,
75
+ Locatable[float],
76
+ Stoppable,
77
+ Flyable,
78
+ Preparable,
79
+ Subscribable[float],
80
+ ):
81
+ """Device that moves a motor record."""
60
82
 
61
83
  def __init__(self, prefix: str, name="") -> None:
62
84
  # Define some signals
63
85
  with self.add_children_as_readables(Format.CONFIG_SIGNAL):
64
86
  self.motor_egu = epics_signal_r(str, prefix + ".EGU")
65
87
  self.velocity = epics_signal_rw(float, prefix + ".VELO")
88
+ self.offset = epics_signal_rw(float, prefix + ".OFF")
66
89
 
67
90
  with self.add_children_as_readables(Format.HINTED_SIGNAL):
68
91
  self.user_readback = epics_signal_r(float, prefix + ".RBV")
@@ -75,11 +98,16 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
75
98
  self.motor_done_move = epics_signal_r(int, prefix + ".DMOV")
76
99
  self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
77
100
  self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
101
+ self.offset_freeze_switch = epics_signal_rw(OffsetMode, prefix + ".FOFF")
102
+ self.high_limit_switch = epics_signal_r(int, prefix + ".HLS")
103
+ self.low_limit_switch = epics_signal_r(int, prefix + ".LLS")
104
+ self.set_use_switch = epics_signal_rw(UseSetMode, prefix + ".SET")
78
105
 
79
106
  # Note:cannot use epics_signal_x here, as the motor record specifies that
80
107
  # we must write 1 to stop the motor. Simply processing the record is not
81
108
  # sufficient.
82
109
  self.motor_stop = epics_signal_w(int, prefix + ".STOP")
110
+
83
111
  # Whether set() should complete successfully or not
84
112
  self._set_success = True
85
113
 
@@ -95,15 +123,14 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
95
123
  super().__init__(name=name)
96
124
 
97
125
  def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
126
+ """Set name of the motor and its children."""
98
127
  super().set_name(name, child_name_separator=child_name_separator)
99
128
  # Readback should be named the same as its parent in read()
100
129
  self.user_readback.set_name(name)
101
130
 
102
131
  @AsyncStatus.wrap
103
132
  async def prepare(self, value: FlyMotorInfo):
104
- """Calculate required velocity and run-up distance, then if motor limits aren't
105
- breached, move to start position minus run-up distance"""
106
-
133
+ """Move to the beginning of a suitable run-up distance ready for a flyscan."""
107
134
  self._fly_timeout = value.timeout
108
135
 
109
136
  # Velocity, at which motor travels from start_position to end_position, in motor
@@ -125,9 +152,9 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
125
152
  @AsyncStatus.wrap
126
153
  async def kickoff(self):
127
154
  """Begin moving motor from prepared position to final position."""
128
- assert (
129
- self._fly_completed_position
130
- ), "Motor must be prepared before attempting to kickoff"
155
+ if not self._fly_completed_position:
156
+ msg = "Motor must be prepared before attempting to kickoff"
157
+ raise RuntimeError(msg)
131
158
 
132
159
  self._fly_status = self.set(
133
160
  self._fly_completed_position, timeout=self._fly_timeout
@@ -135,12 +162,16 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
135
162
 
136
163
  def complete(self) -> WatchableAsyncStatus:
137
164
  """Mark as complete once motor reaches completed position."""
138
- assert self._fly_status, "kickoff not called"
165
+ if not self._fly_status:
166
+ msg = "kickoff not called"
167
+ raise RuntimeError(msg)
139
168
  return self._fly_status
140
169
 
141
170
  @WatchableAsyncStatus.wrap
142
- async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
143
- new_position = value
171
+ async def set( # type: ignore
172
+ self, new_position: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
173
+ ):
174
+ """Move motor to the given value."""
144
175
  self._set_success = True
145
176
  (
146
177
  old_position,
@@ -155,13 +186,18 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
155
186
  self.velocity.get_value(),
156
187
  self.acceleration_time.get_value(),
157
188
  )
189
+
158
190
  if timeout is CALCULATE_TIMEOUT:
159
- assert velocity > 0, "Motor has zero velocity"
160
- timeout = (
161
- abs(new_position - old_position) / velocity
162
- + 2 * acceleration_time
163
- + DEFAULT_TIMEOUT
164
- )
191
+ try:
192
+ timeout = (
193
+ abs((new_position - old_position) / velocity)
194
+ + 2 * acceleration_time
195
+ + DEFAULT_TIMEOUT
196
+ )
197
+ except ZeroDivisionError as error:
198
+ msg = "Mover has zero velocity"
199
+ raise ValueError(msg) from error
200
+
165
201
  move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
166
202
  async for current_position in observe_value(
167
203
  self.user_readback, done_status=move_status
@@ -178,6 +214,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
178
214
  raise RuntimeError("Motor was stopped")
179
215
 
180
216
  async def stop(self, success=False):
217
+ """Request to stop moving and return immediately."""
181
218
  self._set_success = success
182
219
  # Put with completion will never complete as we are waiting for completion on
183
220
  # the move above, so need to pass wait=False
@@ -200,11 +237,19 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
200
237
  return fly_velocity
201
238
 
202
239
  async def locate(self) -> Location[float]:
203
- location: Location = {
204
- "setpoint": await self.user_setpoint.get_value(),
205
- "readback": await self.user_readback.get_value(),
206
- }
207
- return location
240
+ """Return the current setpoint and readback of the motor."""
241
+ setpoint, readback = await asyncio.gather(
242
+ self.user_setpoint.get_value(), self.user_readback.get_value()
243
+ )
244
+ return Location(setpoint=setpoint, readback=readback)
245
+
246
+ def subscribe(self, function: Callback[dict[str, Reading[float]]]) -> None:
247
+ """Subscribe."""
248
+ self.user_readback.subscribe(function)
249
+
250
+ def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None:
251
+ """Unsubscribe."""
252
+ self.user_readback.clear_sub(function)
208
253
 
209
254
  async def _prepare_motor_path(
210
255
  self, fly_velocity: float, start_position: float, end_position: float
@@ -1,4 +1,5 @@
1
- # back compat
1
+ """Back compat."""
2
+
2
3
  import warnings
3
4
 
4
5
  from .core import * # noqa: F403
@@ -9,3 +10,5 @@ warnings.warn(
9
10
  ),
10
11
  stacklevel=2,
11
12
  )
13
+
14
+ __all__ = []
@@ -1,24 +1,24 @@
1
1
  from ._example_ioc import (
2
2
  CA_PVA_RECORDS,
3
3
  PVA_RECORDS,
4
- ExampleCaDevice,
5
- ExampleEnum,
6
- ExamplePvaDevice,
7
- ExampleTable,
8
- connect_example_device,
9
- get_example_ioc,
4
+ EpicsTestCaDevice,
5
+ EpicsTestEnum,
6
+ EpicsTestIocAndDevices,
7
+ EpicsTestPvaDevice,
8
+ EpicsTestSubsetEnum,
9
+ EpicsTestTable,
10
10
  )
11
- from ._utils import TestingIOC, generate_random_PV_prefix
11
+ from ._utils import TestingIOC, generate_random_pv_prefix
12
12
 
13
13
  __all__ = [
14
14
  "CA_PVA_RECORDS",
15
15
  "PVA_RECORDS",
16
- "ExampleCaDevice",
17
- "ExampleEnum",
18
- "ExamplePvaDevice",
19
- "ExampleTable",
20
- "connect_example_device",
21
- "get_example_ioc",
16
+ "EpicsTestCaDevice",
17
+ "EpicsTestEnum",
18
+ "EpicsTestSubsetEnum",
19
+ "EpicsTestPvaDevice",
20
+ "EpicsTestTable",
21
+ "EpicsTestIocAndDevices",
22
22
  "TestingIOC",
23
- "generate_random_PV_prefix",
23
+ "generate_random_pv_prefix",
24
24
  ]
@@ -1,50 +1,57 @@
1
1
  from collections.abc import Sequence
2
2
  from pathlib import Path
3
3
  from typing import Annotated as A
4
- from typing import Literal
5
4
 
6
5
  import numpy as np
7
6
 
8
- from ophyd_async.core import (
9
- Array1D,
10
- SignalRW,
11
- StrictEnum,
12
- Table,
13
- )
14
- from ophyd_async.epics.core import (
15
- EpicsDevice,
16
- PvSuffix,
17
- )
7
+ from ophyd_async.core import Array1D, SignalR, SignalRW, StrictEnum, Table
8
+ from ophyd_async.core._utils import SubsetEnum
9
+ from ophyd_async.epics.core import EpicsDevice, PvSuffix
18
10
 
19
- from ._utils import TestingIOC
11
+ from ._utils import TestingIOC, generate_random_pv_prefix
20
12
 
21
- CA_PVA_RECORDS = str(Path(__file__).parent / "test_records.db")
22
- PVA_RECORDS = str(Path(__file__).parent / "test_records_pva.db")
13
+ CA_PVA_RECORDS = Path(__file__).parent / "test_records.db"
14
+ PVA_RECORDS = Path(__file__).parent / "test_records_pva.db"
23
15
 
24
16
 
25
- class ExampleEnum(StrictEnum):
17
+ class EpicsTestEnum(StrictEnum):
18
+ """For testing strict enum values in test IOCs."""
19
+
26
20
  A = "Aaa"
27
21
  B = "Bbb"
28
22
  C = "Ccc"
29
23
 
30
24
 
31
- class ExampleTable(Table):
32
- bool: Array1D[np.bool_]
33
- int: Array1D[np.int32]
34
- float: Array1D[np.float64]
35
- str: Sequence[str]
36
- enum: Sequence[ExampleEnum]
25
+ class EpicsTestSubsetEnum(SubsetEnum):
26
+ """For testing subset enum values in test IOCs."""
27
+
28
+ A = "Aaa"
29
+ B = "Bbb"
30
+
31
+
32
+ class EpicsTestTable(Table):
33
+ a_bool: Array1D[np.bool_]
34
+ a_int: Array1D[np.int32]
35
+ a_float: Array1D[np.float64]
36
+ a_str: Sequence[str]
37
+ a_enum: Sequence[EpicsTestEnum]
37
38
 
38
39
 
39
- class ExampleCaDevice(EpicsDevice):
40
- my_int: A[SignalRW[int], PvSuffix("int")]
41
- my_float: A[SignalRW[float], PvSuffix("float")]
42
- my_str: A[SignalRW[str], PvSuffix("str")]
40
+ class EpicsTestCaDevice(EpicsDevice):
41
+ """Device for use in a channel access test IOC."""
42
+
43
+ a_int: A[SignalRW[int], PvSuffix("int")]
44
+ """A thing"""
45
+ a_float: A[SignalRW[float], PvSuffix("float")]
46
+ float_prec_0: A[SignalRW[int], PvSuffix("float_prec_0")]
47
+ a_str: A[SignalRW[str], PvSuffix("str")]
43
48
  longstr: A[SignalRW[str], PvSuffix("longstr")]
44
- longstr2: A[SignalRW[str], PvSuffix("longstr2")]
45
- my_bool: A[SignalRW[bool], PvSuffix("bool")]
46
- enum: A[SignalRW[ExampleEnum], PvSuffix("enum")]
47
- enum2: A[SignalRW[ExampleEnum], PvSuffix("enum2")]
49
+ longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
50
+ a_bool: A[SignalRW[bool], PvSuffix("bool")]
51
+ enum: A[SignalRW[EpicsTestEnum], PvSuffix("enum")]
52
+ enum2: A[SignalRW[EpicsTestEnum], PvSuffix("enum2")]
53
+ subset_enum: A[SignalRW[EpicsTestSubsetEnum], PvSuffix("subset_enum")]
54
+ enum_str_fallback: A[SignalRW[str], PvSuffix("enum_str_fallback")]
48
55
  bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
49
56
  partialint: A[SignalRW[int], PvSuffix("partialint")]
50
57
  lessint: A[SignalRW[int], PvSuffix("lessint")]
@@ -56,52 +63,40 @@ class ExampleCaDevice(EpicsDevice):
56
63
  stra: A[SignalRW[Sequence[str]], PvSuffix("stra")]
57
64
 
58
65
 
59
- class ExamplePvaDevice(ExampleCaDevice): # pva can support all signal types that ca can
66
+ class EpicsTestPvaDevice(EpicsTestCaDevice):
67
+ """Device for use in a pv access test IOC."""
68
+
69
+ # pva can support all signal types that ca can
60
70
  int8a: A[SignalRW[Array1D[np.int8]], PvSuffix("int8a")]
61
71
  uint16a: A[SignalRW[Array1D[np.uint16]], PvSuffix("uint16a")]
62
72
  uint32a: A[SignalRW[Array1D[np.uint32]], PvSuffix("uint32a")]
63
73
  int64a: A[SignalRW[Array1D[np.int64]], PvSuffix("int64a")]
64
74
  uint64a: A[SignalRW[Array1D[np.uint64]], PvSuffix("uint64a")]
65
- table: A[SignalRW[ExampleTable], PvSuffix("table")]
66
- ntndarray_data: A[SignalRW[Array1D[np.int64]], PvSuffix("ntndarray:data")]
67
-
68
-
69
- async def connect_example_device(
70
- ioc: TestingIOC, protocol: Literal["ca", "pva"]
71
- ) -> ExamplePvaDevice | ExampleCaDevice:
72
- """Helper function to return a connected example device.
73
-
74
- Parameters
75
- ----------
76
-
77
- ioc: TestingIOC
78
- TestingIOC configured to provide the records needed for the device
79
-
80
- protocol: Literal["ca", "pva"]
81
- The transport protocol of the device
82
-
83
- Returns
84
- -------
85
- ExamplePvaDevice | ExampleCaDevice
86
- a connected EpicsDevice with signals of many EPICS record types
87
- """
88
- device_cls = ExamplePvaDevice if protocol == "pva" else ExampleCaDevice
89
- device = device_cls(f"{protocol}://{ioc.prefix_for(device_cls)}")
90
- await device.connect()
91
- return device
92
-
93
-
94
- def get_example_ioc() -> TestingIOC:
95
- """Get TestingIOC instance with the example databases loaded.
96
-
97
- Returns
98
- -------
99
- TestingIOC
100
- instance with test_records.db loaded for ExampleCaDevice and
101
- test_records.db and test_records_pva.db loaded for ExamplePvaDevice.
102
- """
103
- ioc = TestingIOC()
104
- ioc.database_for(PVA_RECORDS, ExamplePvaDevice)
105
- ioc.database_for(CA_PVA_RECORDS, ExamplePvaDevice)
106
- ioc.database_for(CA_PVA_RECORDS, ExampleCaDevice)
107
- return ioc
75
+ table: A[SignalRW[EpicsTestTable], PvSuffix("table")]
76
+ ntndarray: A[SignalR[np.ndarray], PvSuffix("ntndarray")]
77
+
78
+
79
+ class EpicsTestIocAndDevices:
80
+ """Test IOC with ca and pva devices."""
81
+
82
+ def __init__(self):
83
+ self.prefix = generate_random_pv_prefix()
84
+ self.ioc = TestingIOC()
85
+ # Create supporting records and ExampleCaDevice
86
+ ca_prefix = f"{self.prefix}ca:"
87
+ self.ioc.add_database(CA_PVA_RECORDS, device=ca_prefix)
88
+ self.ca_device = EpicsTestCaDevice(f"ca://{ca_prefix}")
89
+ # Create supporting records and ExamplePvaDevice
90
+ pva_prefix = f"{self.prefix}pva:"
91
+ self.ioc.add_database(CA_PVA_RECORDS, device=pva_prefix)
92
+ self.ioc.add_database(PVA_RECORDS, device=pva_prefix)
93
+ self.pva_device = EpicsTestPvaDevice(f"pva://{pva_prefix}")
94
+
95
+ def get_device(self, protocol: str) -> EpicsTestCaDevice | EpicsTestPvaDevice:
96
+ return getattr(self, f"{protocol}_device")
97
+
98
+ def get_signal(self, protocol: str, name: str) -> SignalRW:
99
+ return getattr(self.get_device(protocol), name)
100
+
101
+ def get_pv(self, protocol: str, name: str) -> str:
102
+ return f"{protocol}://{self.prefix}{protocol}:{name}"
@@ -5,50 +5,31 @@ import sys
5
5
  import time
6
6
  from pathlib import Path
7
7
 
8
- from aioca import purge_channel_caches
9
8
 
10
- from ophyd_async.core import Device
11
-
12
-
13
- def generate_random_PV_prefix() -> str:
9
+ def generate_random_pv_prefix() -> str:
10
+ """For generating random PV names in test devices."""
14
11
  return "".join(random.choice(string.ascii_lowercase) for _ in range(12)) + ":"
15
12
 
16
13
 
17
14
  class TestingIOC:
18
- _dbs: dict[type[Device], list[Path]] = {}
19
- _prefixes: dict[type[Device], str] = {}
20
-
21
- @classmethod
22
- def with_database(cls, db: Path | str): # use as a decorator
23
- def inner(device_cls: type[Device]):
24
- cls.database_for(db, device_cls)
25
- return device_cls
26
-
27
- return inner
15
+ """For initialising an IOC in tests."""
28
16
 
29
- @classmethod
30
- def database_for(cls, db, device_cls):
31
- path = Path(db)
32
- if not path.is_file():
33
- raise OSError(f"{path} is not a file.")
34
- if device_cls not in cls._dbs:
35
- cls._dbs[device_cls] = []
36
- cls._dbs[device_cls].append(path)
17
+ def __init__(self):
18
+ self._db_macros: list[tuple[Path, dict[str, str]]] = []
19
+ self.output = ""
37
20
 
38
- def prefix_for(self, device_cls):
39
- # generate random prefix, return existing if already generated
40
- return self._prefixes.setdefault(device_cls, generate_random_PV_prefix())
21
+ def add_database(self, db: Path | str, /, **macros: str):
22
+ self._db_macros.append((Path(db), macros))
41
23
 
42
- def start_ioc(self):
24
+ def start(self):
43
25
  ioc_args = [
44
26
  sys.executable,
45
27
  "-m",
46
28
  "epicscorelibs.ioc",
47
29
  ]
48
- for device_cls, dbs in self._dbs.items():
49
- prefix = self.prefix_for(device_cls)
50
- for db in dbs:
51
- ioc_args += ["-m", f"device={prefix}", "-d", str(db)]
30
+ for db, macros in self._db_macros:
31
+ macro_str = ",".join(f"{k}={v}" for k, v in macros.items())
32
+ ioc_args += ["-m", macro_str, "-d", str(db)]
52
33
  self._process = subprocess.Popen(
53
34
  ioc_args,
54
35
  stdin=subprocess.PIPE,
@@ -56,23 +37,17 @@ class TestingIOC:
56
37
  stderr=subprocess.STDOUT,
57
38
  universal_newlines=True,
58
39
  )
40
+ assert self._process.stdout # noqa: S101 # this is to make Pylance happy
59
41
  start_time = time.monotonic()
60
- while "iocRun: All initialization complete" not in (
61
- self._process.stdout.readline().strip() # type: ignore
62
- ):
42
+ while "iocRun: All initialization complete" not in self.output:
63
43
  if time.monotonic() - start_time > 10:
64
- try:
65
- print(self._process.communicate("exit()")[0])
66
- except ValueError:
67
- # Someone else already called communicate
68
- pass
69
- raise TimeoutError("IOC did not start in time")
44
+ self.stop()
45
+ raise TimeoutError(f"IOC did not start in time:\n{self.output}")
46
+ self.output += self._process.stdout.readline()
70
47
 
71
- def stop_ioc(self):
72
- # close backend caches before the event loop
73
- purge_channel_caches()
48
+ def stop(self):
74
49
  try:
75
- print(self._process.communicate("exit()")[0])
50
+ self.output += self._process.communicate("exit()")[0]
76
51
  except ValueError:
77
52
  # Someone else already called communicate
78
53
  pass
@@ -96,6 +96,22 @@ record(mbbo, "$(device)enum2") {
96
96
  field(PINI, "YES")
97
97
  }
98
98
 
99
+ record(mbbo, "$(device)subset_enum") {
100
+ field(ZRST, "Aaa")
101
+ field(ONST, "Bbb")
102
+ field(TWST, "Ccc")
103
+ field(VAL, "1")
104
+ field(PINI, "YES")
105
+ }
106
+
107
+ record(mbbo, "$(device)enum_str_fallback") {
108
+ field(ZRST, "Aaa")
109
+ field(ONST, "Bbb")
110
+ field(TWST, "Ccc")
111
+ field(VAL, "1")
112
+ field(PINI, "YES")
113
+ }
114
+
99
115
  record(waveform, "$(device)uint8a") {
100
116
  field(NELM, "3")
101
117
  field(FTVL, "UCHAR")
@@ -1,36 +1,37 @@
1
1
  record(waveform, "$(device)int8a") {
2
- field(NELM, "3")
2
+ field(NELM, "7")
3
3
  field(FTVL, "CHAR")
4
- field(INP, {const:[-128, 127]})
4
+ field(INP, {const:[-128, 127, 0, 1, 2, 3, 4]})
5
5
  field(PINI, "YES")
6
6
  }
7
7
 
8
8
  record(waveform, "$(device)uint16a") {
9
- field(NELM, "3")
9
+ field(NELM, "7")
10
10
  field(FTVL, "USHORT")
11
- field(INP, {const:[0, 65535]})
11
+ field(INP, {const:[0, 65535, 0, 1, 2, 3, 4]})
12
12
  field(PINI, "YES")
13
13
  }
14
14
 
15
15
  record(waveform, "$(device)uint32a") {
16
- field(NELM, "3")
16
+ field(NELM, "7")
17
17
  field(FTVL, "ULONG")
18
- field(INP, {const:[0, 4294967295]})
18
+ field(INP, {const:[0, 4294967295, 0, 1, 2, 3, 4]})
19
19
  field(PINI, "YES")
20
20
  }
21
21
 
22
22
  record(waveform, "$(device)int64a") {
23
- field(NELM, "3")
23
+ field(NELM, "7")
24
24
  field(FTVL, "INT64")
25
- # Can't do 64-bit int with JSON numbers in a const link...
26
- field(INP, {const:[-2147483649, 2147483648]})
25
+ # limit of range appears to be +/-(2^63 - 1)
26
+ field(INP, {const:[-9223372036854775807, 9223372036854775807, 0, 1, 2, 3, 4]})
27
27
  field(PINI, "YES")
28
28
  }
29
29
 
30
30
  record(waveform, "$(device)uint64a") {
31
- field(NELM, "3")
31
+ field(NELM, "7")
32
32
  field(FTVL, "UINT64")
33
- field(INP, {const:[0, 4294967297]})
33
+ # limit of range appears to be 0 to +(2^63 - 1)
34
+ field(INP, {const:[0, 9223372036854775807, 0, 1, 2, 3, 4]})
34
35
  field(PINI, "YES")
35
36
  }
36
37
 
@@ -58,7 +59,7 @@ record(waveform, "$(device)table:bool")
58
59
  field(PINI, "YES")
59
60
  info(Q:group, {
60
61
  "$(device)table": {
61
- "value.bool": {
62
+ "value.a_bool": {
62
63
  "+type": "plain",
63
64
  "+channel": "VAL",
64
65
  "+putorder": 1
@@ -75,7 +76,7 @@ record(waveform, "$(device)table:int")
75
76
  field(PINI, "YES")
76
77
  info(Q:group, {
77
78
  "$(device)table": {
78
- "value.int": {
79
+ "value.a_int": {
79
80
  "+type": "plain",
80
81
  "+channel": "VAL",
81
82
  "+putorder": 2
@@ -92,7 +93,7 @@ record(waveform, "$(device)table:float")
92
93
  field(PINI, "YES")
93
94
  info(Q:group, {
94
95
  "$(device)table": {
95
- "value.float": {
96
+ "value.a_float": {
96
97
  "+type": "plain",
97
98
  "+channel": "VAL",
98
99
  "+putorder": 3
@@ -109,7 +110,7 @@ record(waveform, "$(device)table:str")
109
110
  field(PINI, "YES")
110
111
  info(Q:group, {
111
112
  "$(device)table": {
112
- "value.str": {
113
+ "value.a_str": {
113
114
  "+type": "plain",
114
115
  "+channel": "VAL",
115
116
  "+putorder": 4
@@ -126,7 +127,7 @@ record(waveform, "$(device)table:enum")
126
127
  field(PINI, "YES")
127
128
  info(Q:group, {
128
129
  "$(device)table": {
129
- "value.enum": {
130
+ "value.a_enum": {
130
131
  "+type": "plain",
131
132
  "+channel": "VAL",
132
133
  "+putorder": 5,
@@ -0,0 +1 @@
1
+ """FastCS support for Signals via EPICS or Tango, and Devices that use them."""