ophyd-async 0.9.0a2__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 (151) 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 +97 -62
  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 +106 -125
  8. ophyd_async/core/_device.py +69 -63
  9. ophyd_async/core/_device_filler.py +65 -1
  10. ophyd_async/core/_flyer.py +14 -5
  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 +44 -35
  17. ophyd_async/core/_settings.py +36 -27
  18. ophyd_async/core/_signal.py +262 -170
  19. ophyd_async/core/_signal_backend.py +56 -13
  20. ophyd_async/core/_soft_signal_backend.py +16 -11
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +37 -8
  23. ophyd_async/core/_utils.py +96 -49
  24. ophyd_async/core/_yaml_settings.py +2 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/_andor.py +2 -2
  27. ophyd_async/epics/adandor/_andor_controller.py +4 -2
  28. ophyd_async/epics/adandor/_andor_io.py +2 -4
  29. ophyd_async/epics/adaravis/__init__.py +5 -0
  30. ophyd_async/epics/adaravis/_aravis.py +4 -8
  31. ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
  32. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  33. ophyd_async/epics/adcore/__init__.py +23 -8
  34. ophyd_async/epics/adcore/_core_detector.py +42 -2
  35. ophyd_async/epics/adcore/_core_io.py +124 -99
  36. ophyd_async/epics/adcore/_core_logic.py +106 -27
  37. ophyd_async/epics/adcore/_core_writer.py +12 -8
  38. ophyd_async/epics/adcore/_hdf_writer.py +21 -38
  39. ophyd_async/epics/adcore/_single_trigger.py +2 -2
  40. ophyd_async/epics/adcore/_utils.py +2 -2
  41. ophyd_async/epics/adkinetix/__init__.py +2 -1
  42. ophyd_async/epics/adkinetix/_kinetix.py +3 -3
  43. ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
  44. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  45. ophyd_async/epics/adpilatus/__init__.py +5 -0
  46. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  47. ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
  48. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  49. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  50. ophyd_async/epics/adsimdetector/_sim.py +4 -14
  51. ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
  52. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  53. ophyd_async/epics/advimba/__init__.py +10 -1
  54. ophyd_async/epics/advimba/_vimba.py +3 -2
  55. ophyd_async/epics/advimba/_vimba_controller.py +4 -2
  56. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  57. ophyd_async/epics/core/_aioca.py +35 -16
  58. ophyd_async/epics/core/_epics_connector.py +4 -0
  59. ophyd_async/epics/core/_epics_device.py +2 -0
  60. ophyd_async/epics/core/_p4p.py +10 -2
  61. ophyd_async/epics/core/_pvi_connector.py +65 -8
  62. ophyd_async/epics/core/_signal.py +51 -51
  63. ophyd_async/epics/core/_util.py +4 -4
  64. ophyd_async/epics/demo/__init__.py +16 -0
  65. ophyd_async/epics/demo/__main__.py +31 -0
  66. ophyd_async/epics/demo/_ioc.py +32 -0
  67. ophyd_async/epics/demo/_motor.py +82 -0
  68. ophyd_async/epics/demo/_point_detector.py +42 -0
  69. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  70. ophyd_async/epics/demo/_stage.py +15 -0
  71. ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
  72. ophyd_async/epics/demo/point_detector.db +59 -0
  73. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  74. ophyd_async/epics/eiger/_eiger.py +1 -3
  75. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  76. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  77. ophyd_async/epics/eiger/_odin_io.py +1 -2
  78. ophyd_async/epics/motor.py +65 -28
  79. ophyd_async/epics/signal.py +4 -1
  80. ophyd_async/epics/testing/_example_ioc.py +21 -9
  81. ophyd_async/epics/testing/_utils.py +3 -0
  82. ophyd_async/epics/testing/test_records.db +8 -0
  83. ophyd_async/epics/testing/test_records_pva.db +17 -16
  84. ophyd_async/fastcs/__init__.py +1 -0
  85. ophyd_async/fastcs/core.py +6 -0
  86. ophyd_async/fastcs/odin/__init__.py +1 -0
  87. ophyd_async/fastcs/panda/__init__.py +8 -6
  88. ophyd_async/fastcs/panda/_block.py +29 -9
  89. ophyd_async/fastcs/panda/_control.py +5 -0
  90. ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
  91. ophyd_async/fastcs/panda/_table.py +9 -6
  92. ophyd_async/fastcs/panda/_trigger.py +23 -9
  93. ophyd_async/fastcs/panda/_writer.py +27 -30
  94. ophyd_async/plan_stubs/__init__.py +2 -0
  95. ophyd_async/plan_stubs/_ensure_connected.py +1 -0
  96. ophyd_async/plan_stubs/_fly.py +2 -4
  97. ophyd_async/plan_stubs/_nd_attributes.py +2 -0
  98. ophyd_async/plan_stubs/_panda.py +1 -0
  99. ophyd_async/plan_stubs/_settings.py +43 -16
  100. ophyd_async/plan_stubs/_utils.py +3 -0
  101. ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
  102. ophyd_async/sim/__init__.py +24 -14
  103. ophyd_async/sim/__main__.py +43 -0
  104. ophyd_async/sim/_blob_detector.py +33 -0
  105. ophyd_async/sim/_blob_detector_controller.py +48 -0
  106. ophyd_async/sim/_blob_detector_writer.py +105 -0
  107. ophyd_async/sim/_mirror_horizontal.py +46 -0
  108. ophyd_async/sim/_mirror_vertical.py +74 -0
  109. ophyd_async/sim/_motor.py +233 -0
  110. ophyd_async/sim/_pattern_generator.py +124 -0
  111. ophyd_async/sim/_point_detector.py +86 -0
  112. ophyd_async/sim/_stage.py +19 -0
  113. ophyd_async/tango/__init__.py +1 -0
  114. ophyd_async/tango/core/__init__.py +6 -1
  115. ophyd_async/tango/core/_base_device.py +41 -33
  116. ophyd_async/tango/core/_converters.py +81 -0
  117. ophyd_async/tango/core/_signal.py +18 -32
  118. ophyd_async/tango/core/_tango_readable.py +2 -19
  119. ophyd_async/tango/core/_tango_transport.py +136 -60
  120. ophyd_async/tango/core/_utils.py +47 -0
  121. ophyd_async/tango/{sim → demo}/_counter.py +2 -0
  122. ophyd_async/tango/{sim → demo}/_detector.py +2 -0
  123. ophyd_async/tango/{sim → demo}/_mover.py +5 -4
  124. ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
  125. ophyd_async/tango/testing/__init__.py +6 -0
  126. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  127. ophyd_async/testing/__init__.py +29 -7
  128. ophyd_async/testing/_assert.py +137 -81
  129. ophyd_async/testing/_mock_signal_utils.py +56 -70
  130. ophyd_async/testing/_one_of_everything.py +41 -21
  131. ophyd_async/testing/_single_derived.py +87 -0
  132. ophyd_async/testing/_utils.py +3 -0
  133. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
  134. ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
  135. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
  136. ophyd_async/epics/sim/__init__.py +0 -54
  137. ophyd_async/epics/sim/_ioc.py +0 -29
  138. ophyd_async/epics/sim/_mover.py +0 -101
  139. ophyd_async/epics/sim/_sensor.py +0 -37
  140. ophyd_async/epics/sim/sensor.db +0 -19
  141. ophyd_async/sim/_pattern_detector/__init__.py +0 -13
  142. ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
  143. ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
  144. ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
  145. ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
  146. ophyd_async/sim/_sim_motor.py +0 -107
  147. ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
  148. /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
  149. /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
  150. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
  151. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,22 @@
1
+ from typing import Annotated as A
2
+
3
+ from ophyd_async.core import SignalR, SignalRW, StandardReadable, StrictEnum
4
+ from ophyd_async.core import StandardReadableFormat as Format
5
+ from ophyd_async.epics.core import EpicsDevice, PvSuffix
6
+
7
+
8
+ class EnergyMode(StrictEnum):
9
+ """Energy mode for `DemoPointDetectorChannel`."""
10
+
11
+ LOW = "Low Energy"
12
+ """Low energy mode"""
13
+
14
+ HIGH = "High Energy"
15
+ """High energy mode"""
16
+
17
+
18
+ class DemoPointDetectorChannel(StandardReadable, EpicsDevice):
19
+ """A channel for `DemoPointDetector` with int value based on X and Y Motors."""
20
+
21
+ value: A[SignalR[int], PvSuffix("Value"), Format.HINTED_UNCACHED_SIGNAL]
22
+ mode: A[SignalRW[EnergyMode], PvSuffix("Mode"), Format.CONFIG_SIGNAL]
@@ -0,0 +1,15 @@
1
+ from ophyd_async.core import StandardReadable
2
+
3
+ from ._motor import DemoMotor
4
+
5
+
6
+ class DemoStage(StandardReadable):
7
+ """A simulated sample stage with X and Y movables."""
8
+
9
+ def __init__(self, prefix: str, name="") -> None:
10
+ # Define some child Devices
11
+ with self.add_children_as_readables():
12
+ self.x = DemoMotor(prefix + "X:")
13
+ self.y = DemoMotor(prefix + "Y:")
14
+ # Set name of device and child devices
15
+ super().__init__(name=name)
@@ -10,7 +10,8 @@ record(ao, "$(P)Velocity") {
10
10
  field(PREC, "$(PREC=3)")
11
11
  field(PINI, "YES")
12
12
  field(EGU, "$(EGU=mm)/s")
13
- field(VAL, "$(VELO=100)")
13
+ field(VAL, "$(VELO=1)")
14
+ field(DRVL, "0")
14
15
  }
15
16
 
16
17
  record(calc, "$(P)VelocityDiv") {
@@ -0,0 +1,59 @@
1
+ record(ao, "$(P)AcquireTime") {
2
+ field(DESC, "Time to acquire for")
3
+ field(VAL, "0.1")
4
+ field(OUT, "$(P)Start.DLY2")
5
+ field(PINI, "YES")
6
+ }
7
+
8
+ record(seq, "$(P)Start") {
9
+ field(DESC, "Start sequence")
10
+ # Grab the start time
11
+ field(LNK0, "$(P)StartTime.PROC")
12
+ # Set it to be acquiring
13
+ field(LNK1, "$(P)Acquiring PP")
14
+ field(DO1, "1")
15
+ # Set it back to idle
16
+ field(LNK2, "$(P)Acquiring PP")
17
+ field(DO2, "0")
18
+ # Set the elapsed time to the full acquire time
19
+ field(LNK3, "$(P)Elapsed PP")
20
+ field(DOL3, "$(P)AcquireTime")
21
+ }
22
+
23
+ record(ai, "$(P)StartTime") {
24
+ field(DTYP, "Soft Timestamp")
25
+ }
26
+
27
+ record(bi, "$(P)Acquiring") {
28
+ field(DESC, "Currently acquiring")
29
+ field(ZNAM, "Idle")
30
+ field(ONAM, "Acquiring")
31
+ field(PINI, "YES")
32
+ }
33
+
34
+ record(ai, "$(P)CurrentTime") {
35
+ field(DTYP, "Soft Timestamp")
36
+ }
37
+
38
+ record(calcout, "$(P)Process") {
39
+ field(DESC, "Process elapsed time if acquiring")
40
+ field(INPA, "$(P)StartTime")
41
+ field(INPB, "$(P)CurrentTime PP")
42
+ field(SCAN, ".1 second")
43
+ field(CALC, "B-A")
44
+ field(OUT, "$(P)Elapsed PP")
45
+ field(SDIS, "$(P)Acquiring")
46
+ field(DISV, "0")
47
+ }
48
+
49
+ record(ai, "$(P)Elapsed") {
50
+ field(DESC, "Elapsed time")
51
+ field(EGU, "s")
52
+ field(PREC, "1")
53
+ field(PINI, "YES")
54
+ }
55
+
56
+ record(calcout, "$(P)Reset") {
57
+ field(OUT, "$(P)Elapsed PP")
58
+ field(CALC, "0")
59
+ }
@@ -0,0 +1,21 @@
1
+ record(mbbo, "$(P)$(CHANNEL):Mode") {
2
+ field(DESC, "Energy sensitivity of the image")
3
+ field(DTYP, "Raw Soft Channel")
4
+ field(PINI, "YES")
5
+ field(ZRVL, "10")
6
+ field(ZRST, "Low Energy")
7
+ field(ONVL, "100")
8
+ field(ONST, "High Energy")
9
+ }
10
+
11
+ record(calc, "$(P)$(CHANNEL):Value") {
12
+ field(DESC, "Sensor value simulated from X and Y")
13
+ field(INPA, "$(X)Readback")
14
+ field(INPB, "$(Y)Readback")
15
+ field(INPC, "$(CHANNEL)")
16
+ field(INPD, "$(P)$(CHANNEL):Mode.RVAL")
17
+ field(INPE, "$(P)Elapsed CP")
18
+ field(CALC, "FLOOR((SIN(A)**C+COS(A*B+D)+2)*2500*E)")
19
+ field(EGU, "cts")
20
+ field(PREC, "0")
21
+ }
@@ -12,9 +12,7 @@ class EigerTriggerInfo(TriggerInfo):
12
12
 
13
13
 
14
14
  class EigerDetector(StandardDetector):
15
- """
16
- Ophyd-async implementation of an Eiger Detector.
17
- """
15
+ """Ophyd-async implementation of an Eiger Detector."""
18
16
 
19
17
  _controller: EigerController
20
18
  _writer: Odin
@@ -2,6 +2,7 @@ import asyncio
2
2
 
3
3
  from ophyd_async.core import (
4
4
  DEFAULT_TIMEOUT,
5
+ AsyncStatus,
5
6
  DetectorController,
6
7
  DetectorTrigger,
7
8
  TriggerInfo,
@@ -19,19 +20,24 @@ EIGER_TRIGGER_MODE_MAP = {
19
20
 
20
21
 
21
22
  class EigerController(DetectorController):
23
+ """Controller for the Eiger detector."""
24
+
22
25
  def __init__(
23
26
  self,
24
27
  driver: EigerDriverIO,
25
28
  ) -> None:
26
29
  self._drv = driver
30
+ self._arm_status: AsyncStatus | None = None
27
31
 
28
32
  def get_deadtime(self, exposure: float | None) -> float:
29
33
  # See https://media.dectris.com/filer_public/30/14/3014704e-5f3b-43ba-8ccf-8ef720e60d2a/240202_usermanual_eiger2.pdf
30
34
  return 0.0001
31
35
 
32
36
  async def set_energy(self, energy: float, tolerance: float = 0.1):
33
- """Changing photon energy takes some time so only do so if the current energy is
34
- outside the tolerance."""
37
+ """Change photon energy if outside tolerance.
38
+
39
+ It takes some time so don't do it unless it is outside tolerance.
40
+ """
35
41
  current_energy = await self._drv.photon_energy.get_value()
36
42
  if abs(current_energy - energy) > tolerance:
37
43
  await self._drv.photon_energy.set(energy)
@@ -54,7 +60,7 @@ class EigerController(DetectorController):
54
60
 
55
61
  async def arm(self):
56
62
  # TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
57
- self._arm_status = set_and_wait_for_other_value(
63
+ self._arm_status = await set_and_wait_for_other_value(
58
64
  self._drv.arm,
59
65
  1,
60
66
  self._drv.state,
@@ -64,8 +70,9 @@ class EigerController(DetectorController):
64
70
  )
65
71
 
66
72
  async def wait_for_idle(self):
67
- if self._arm_status:
73
+ if self._arm_status and not self._arm_status.done:
68
74
  await self._arm_status
75
+ self._arm_status = None
69
76
 
70
77
  async def disarm(self):
71
78
  await self._drv.disarm.set(1)
@@ -9,6 +9,8 @@ class EigerTriggerMode(StrictEnum):
9
9
 
10
10
 
11
11
  class EigerDriverIO(Device):
12
+ """Contains signals for handling IO on the Eiger detector."""
13
+
12
14
  def __init__(self, prefix: str, name: str = "") -> None:
13
15
  self.bit_depth = epics_signal_r(int, f"{prefix}BitDepthReadout")
14
16
  self.stale_parameters = epics_signal_r(bool, f"{prefix}StaleParameters")
@@ -5,7 +5,6 @@ from bluesky.protocols import StreamAsset
5
5
  from event_model import DataKey
6
6
 
7
7
  from ophyd_async.core import (
8
- DEFAULT_TIMEOUT,
9
8
  DetectorWriter,
10
9
  Device,
11
10
  DeviceVector,
@@ -110,7 +109,7 @@ class OdinWriter(DetectorWriter):
110
109
  }
111
110
 
112
111
  async def observe_indices_written(
113
- self, timeout=DEFAULT_TIMEOUT
112
+ self, timeout: float
114
113
  ) -> AsyncGenerator[int, None]:
115
114
  async for num_captured in observe_value(self._drv.num_captured, timeout):
116
115
  yield num_captured
@@ -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,41 +31,54 @@ 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"
66
+
56
67
 
68
+ class UseSetMode(StrictEnum):
69
+ USE = "Use"
70
+ SET = "Set"
57
71
 
58
- class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
59
- """Device that moves a motor record"""
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
@@ -76,11 +98,16 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
76
98
  self.motor_done_move = epics_signal_r(int, prefix + ".DMOV")
77
99
  self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
78
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")
79
105
 
80
106
  # Note:cannot use epics_signal_x here, as the motor record specifies that
81
107
  # we must write 1 to stop the motor. Simply processing the record is not
82
108
  # sufficient.
83
109
  self.motor_stop = epics_signal_w(int, prefix + ".STOP")
110
+
84
111
  # Whether set() should complete successfully or not
85
112
  self._set_success = True
86
113
 
@@ -96,15 +123,14 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
96
123
  super().__init__(name=name)
97
124
 
98
125
  def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
126
+ """Set name of the motor and its children."""
99
127
  super().set_name(name, child_name_separator=child_name_separator)
100
128
  # Readback should be named the same as its parent in read()
101
129
  self.user_readback.set_name(name)
102
130
 
103
131
  @AsyncStatus.wrap
104
132
  async def prepare(self, value: FlyMotorInfo):
105
- """Calculate required velocity and run-up distance, then if motor limits aren't
106
- breached, move to start position minus run-up distance"""
107
-
133
+ """Move to the beginning of a suitable run-up distance ready for a flyscan."""
108
134
  self._fly_timeout = value.timeout
109
135
 
110
136
  # Velocity, at which motor travels from start_position to end_position, in motor
@@ -142,8 +168,10 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
142
168
  return self._fly_status
143
169
 
144
170
  @WatchableAsyncStatus.wrap
145
- async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
146
- 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."""
147
175
  self._set_success = True
148
176
  (
149
177
  old_position,
@@ -186,6 +214,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
186
214
  raise RuntimeError("Motor was stopped")
187
215
 
188
216
  async def stop(self, success=False):
217
+ """Request to stop moving and return immediately."""
189
218
  self._set_success = success
190
219
  # Put with completion will never complete as we are waiting for completion on
191
220
  # the move above, so need to pass wait=False
@@ -208,11 +237,19 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
208
237
  return fly_velocity
209
238
 
210
239
  async def locate(self) -> Location[float]:
211
- location: Location = {
212
- "setpoint": await self.user_setpoint.get_value(),
213
- "readback": await self.user_readback.get_value(),
214
- }
215
- 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)
216
253
 
217
254
  async def _prepare_motor_path(
218
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__ = []
@@ -15,35 +15,43 @@ PVA_RECORDS = Path(__file__).parent / "test_records_pva.db"
15
15
 
16
16
 
17
17
  class EpicsTestEnum(StrictEnum):
18
+ """For testing strict enum values in test IOCs."""
19
+
18
20
  A = "Aaa"
19
21
  B = "Bbb"
20
22
  C = "Ccc"
21
23
 
22
24
 
23
25
  class EpicsTestSubsetEnum(SubsetEnum):
26
+ """For testing subset enum values in test IOCs."""
27
+
24
28
  A = "Aaa"
25
29
  B = "Bbb"
26
30
 
27
31
 
28
32
  class EpicsTestTable(Table):
29
- bool: Array1D[np.bool_]
30
- int: Array1D[np.int32]
31
- float: Array1D[np.float64]
32
- str: Sequence[str]
33
- enum: Sequence[EpicsTestEnum]
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]
34
38
 
35
39
 
36
40
  class EpicsTestCaDevice(EpicsDevice):
37
- my_int: A[SignalRW[int], PvSuffix("int")]
38
- my_float: A[SignalRW[float], PvSuffix("float")]
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")]
39
46
  float_prec_0: A[SignalRW[int], PvSuffix("float_prec_0")]
40
- my_str: A[SignalRW[str], PvSuffix("str")]
47
+ a_str: A[SignalRW[str], PvSuffix("str")]
41
48
  longstr: A[SignalRW[str], PvSuffix("longstr")]
42
49
  longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
43
- my_bool: A[SignalRW[bool], PvSuffix("bool")]
50
+ a_bool: A[SignalRW[bool], PvSuffix("bool")]
44
51
  enum: A[SignalRW[EpicsTestEnum], PvSuffix("enum")]
45
52
  enum2: A[SignalRW[EpicsTestEnum], PvSuffix("enum2")]
46
53
  subset_enum: A[SignalRW[EpicsTestSubsetEnum], PvSuffix("subset_enum")]
54
+ enum_str_fallback: A[SignalRW[str], PvSuffix("enum_str_fallback")]
47
55
  bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
48
56
  partialint: A[SignalRW[int], PvSuffix("partialint")]
49
57
  lessint: A[SignalRW[int], PvSuffix("lessint")]
@@ -56,6 +64,8 @@ class EpicsTestCaDevice(EpicsDevice):
56
64
 
57
65
 
58
66
  class EpicsTestPvaDevice(EpicsTestCaDevice):
67
+ """Device for use in a pv access test IOC."""
68
+
59
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")]
@@ -67,6 +77,8 @@ class EpicsTestPvaDevice(EpicsTestCaDevice):
67
77
 
68
78
 
69
79
  class EpicsTestIocAndDevices:
80
+ """Test IOC with ca and pva devices."""
81
+
70
82
  def __init__(self):
71
83
  self.prefix = generate_random_pv_prefix()
72
84
  self.ioc = TestingIOC()
@@ -7,10 +7,13 @@ from pathlib import Path
7
7
 
8
8
 
9
9
  def generate_random_pv_prefix() -> str:
10
+ """For generating random PV names in test devices."""
10
11
  return "".join(random.choice(string.ascii_lowercase) for _ in range(12)) + ":"
11
12
 
12
13
 
13
14
  class TestingIOC:
15
+ """For initialising an IOC in tests."""
16
+
14
17
  def __init__(self):
15
18
  self._db_macros: list[tuple[Path, dict[str, str]]] = []
16
19
  self.output = ""
@@ -104,6 +104,14 @@ record(mbbo, "$(device)subset_enum") {
104
104
  field(PINI, "YES")
105
105
  }
106
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
+
107
115
  record(waveform, "$(device)uint8a") {
108
116
  field(NELM, "3")
109
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."""
@@ -1,9 +1,15 @@
1
+ """FastCS core module for ophyd-async."""
2
+
1
3
  from ophyd_async.core import Device, DeviceConnector
2
4
  from ophyd_async.epics.core import PviDeviceConnector
3
5
 
4
6
 
5
7
  def fastcs_connector(device: Device, uri: str, error_hint: str = "") -> DeviceConnector:
8
+ """Create devices and connections on pvi device `Device`."""
6
9
  # TODO: add Tango support based on uri scheme
7
10
  connector = PviDeviceConnector(uri, error_hint)
8
11
  connector.create_children_from_annotations(device)
9
12
  return connector
13
+
14
+
15
+ __all__ = ["fastcs_connector"]
@@ -0,0 +1 @@
1
+ __all__ = []
@@ -1,13 +1,14 @@
1
1
  from ._block import (
2
- BitMux,
3
2
  CommonPandaBlocks,
4
3
  DataBlock,
4
+ PandaBitMux,
5
+ PandaCaptureMode,
6
+ PandaPcompDirection,
7
+ PandaTimeUnits,
5
8
  PcapBlock,
6
9
  PcompBlock,
7
- PcompDirection,
8
10
  PulseBlock,
9
11
  SeqBlock,
10
- TimeUnits,
11
12
  )
12
13
  from ._control import PandaPcapController
13
14
  from ._hdf_panda import HDFPanda
@@ -28,13 +29,14 @@ from ._writer import PandaHDFWriter
28
29
  __all__ = [
29
30
  "CommonPandaBlocks",
30
31
  "DataBlock",
31
- "BitMux",
32
+ "PandaBitMux",
33
+ "PandaCaptureMode",
32
34
  "PcapBlock",
33
35
  "PcompBlock",
34
- "PcompDirection",
36
+ "PandaPcompDirection",
35
37
  "PulseBlock",
36
38
  "SeqBlock",
37
- "TimeUnits",
39
+ "PandaTimeUnits",
38
40
  "HDFPanda",
39
41
  "PandaHDFWriter",
40
42
  "PandaPcapController",