ophyd-async 0.9.0a1__py3-none-any.whl → 0.9.0a2__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 (97) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +13 -20
  3. ophyd_async/core/_detector.py +61 -37
  4. ophyd_async/core/_device.py +102 -80
  5. ophyd_async/core/_device_filler.py +17 -8
  6. ophyd_async/core/_flyer.py +2 -2
  7. ophyd_async/core/_readable.py +30 -23
  8. ophyd_async/core/_settings.py +104 -0
  9. ophyd_async/core/_signal.py +55 -17
  10. ophyd_async/core/_signal_backend.py +4 -1
  11. ophyd_async/core/_soft_signal_backend.py +2 -1
  12. ophyd_async/core/_table.py +18 -10
  13. ophyd_async/core/_utils.py +5 -3
  14. ophyd_async/core/_yaml_settings.py +64 -0
  15. ophyd_async/epics/adandor/__init__.py +9 -0
  16. ophyd_async/epics/adandor/_andor.py +45 -0
  17. ophyd_async/epics/adandor/_andor_controller.py +49 -0
  18. ophyd_async/epics/adandor/_andor_io.py +36 -0
  19. ophyd_async/epics/adaravis/__init__.py +3 -1
  20. ophyd_async/epics/adaravis/_aravis.py +23 -37
  21. ophyd_async/epics/adaravis/_aravis_controller.py +13 -22
  22. ophyd_async/epics/adcore/__init__.py +15 -8
  23. ophyd_async/epics/adcore/_core_detector.py +41 -0
  24. ophyd_async/epics/adcore/_core_io.py +35 -10
  25. ophyd_async/epics/adcore/_core_logic.py +98 -86
  26. ophyd_async/epics/adcore/_core_writer.py +219 -0
  27. ophyd_async/epics/adcore/_hdf_writer.py +38 -62
  28. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  29. ophyd_async/epics/adcore/_single_trigger.py +4 -3
  30. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  31. ophyd_async/epics/adcore/_utils.py +2 -1
  32. ophyd_async/epics/adkinetix/_kinetix.py +29 -24
  33. ophyd_async/epics/adkinetix/_kinetix_controller.py +9 -21
  34. ophyd_async/epics/adpilatus/__init__.py +2 -2
  35. ophyd_async/epics/adpilatus/_pilatus.py +27 -39
  36. ophyd_async/epics/adpilatus/_pilatus_controller.py +44 -22
  37. ophyd_async/epics/adsimdetector/__init__.py +3 -3
  38. ophyd_async/epics/adsimdetector/_sim.py +33 -17
  39. ophyd_async/epics/advimba/_vimba.py +23 -23
  40. ophyd_async/epics/advimba/_vimba_controller.py +10 -24
  41. ophyd_async/epics/core/_aioca.py +31 -14
  42. ophyd_async/epics/core/_p4p.py +40 -16
  43. ophyd_async/epics/core/_util.py +1 -1
  44. ophyd_async/epics/motor.py +18 -10
  45. ophyd_async/epics/sim/_ioc.py +29 -0
  46. ophyd_async/epics/{demo → sim}/_mover.py +10 -4
  47. ophyd_async/epics/testing/__init__.py +14 -14
  48. ophyd_async/epics/testing/_example_ioc.py +48 -65
  49. ophyd_async/epics/testing/_utils.py +17 -45
  50. ophyd_async/epics/testing/test_records.db +8 -0
  51. ophyd_async/fastcs/panda/__init__.py +0 -2
  52. ophyd_async/fastcs/panda/_control.py +7 -2
  53. ophyd_async/fastcs/panda/_hdf_panda.py +3 -1
  54. ophyd_async/fastcs/panda/_table.py +4 -1
  55. ophyd_async/plan_stubs/__init__.py +14 -0
  56. ophyd_async/plan_stubs/_ensure_connected.py +11 -17
  57. ophyd_async/plan_stubs/_fly.py +1 -1
  58. ophyd_async/plan_stubs/_nd_attributes.py +7 -5
  59. ophyd_async/plan_stubs/_panda.py +13 -0
  60. ophyd_async/plan_stubs/_settings.py +125 -0
  61. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  62. ophyd_async/sim/__init__.py +19 -0
  63. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
  64. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
  65. ophyd_async/tango/core/_signal.py +3 -1
  66. ophyd_async/tango/core/_tango_transport.py +12 -14
  67. ophyd_async/tango/{demo → sim}/_mover.py +5 -2
  68. ophyd_async/testing/__init__.py +19 -0
  69. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  70. ophyd_async/testing/_assert.py +88 -40
  71. ophyd_async/testing/_mock_signal_utils.py +3 -3
  72. ophyd_async/testing/_one_of_everything.py +126 -0
  73. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/METADATA +2 -2
  74. ophyd_async-0.9.0a2.dist-info/RECORD +129 -0
  75. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/WHEEL +1 -1
  76. ophyd_async/core/_device_save_loader.py +0 -274
  77. ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
  78. ophyd_async/fastcs/panda/_utils.py +0 -16
  79. ophyd_async/sim/demo/__init__.py +0 -19
  80. ophyd_async/sim/testing/__init__.py +0 -0
  81. ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
  82. ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
  83. /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
  84. /ophyd_async/epics/{demo → sim}/_sensor.py +0 -0
  85. /ophyd_async/epics/{demo → sim}/mover.db +0 -0
  86. /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
  87. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
  88. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
  89. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
  90. /ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +0 -0
  91. /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
  92. /ophyd_async/tango/{demo → sim}/_counter.py +0 -0
  93. /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
  94. /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
  95. /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
  96. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/LICENSE +0 -0
  97. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/top_level.txt +0 -0
@@ -63,6 +63,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
63
63
  with self.add_children_as_readables(Format.CONFIG_SIGNAL):
64
64
  self.motor_egu = epics_signal_r(str, prefix + ".EGU")
65
65
  self.velocity = epics_signal_rw(float, prefix + ".VELO")
66
+ self.offset = epics_signal_rw(float, prefix + ".OFF")
66
67
 
67
68
  with self.add_children_as_readables(Format.HINTED_SIGNAL):
68
69
  self.user_readback = epics_signal_r(float, prefix + ".RBV")
@@ -125,9 +126,9 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
125
126
  @AsyncStatus.wrap
126
127
  async def kickoff(self):
127
128
  """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"
129
+ if not self._fly_completed_position:
130
+ msg = "Motor must be prepared before attempting to kickoff"
131
+ raise RuntimeError(msg)
131
132
 
132
133
  self._fly_status = self.set(
133
134
  self._fly_completed_position, timeout=self._fly_timeout
@@ -135,7 +136,9 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
135
136
 
136
137
  def complete(self) -> WatchableAsyncStatus:
137
138
  """Mark as complete once motor reaches completed position."""
138
- assert self._fly_status, "kickoff not called"
139
+ if not self._fly_status:
140
+ msg = "kickoff not called"
141
+ raise RuntimeError(msg)
139
142
  return self._fly_status
140
143
 
141
144
  @WatchableAsyncStatus.wrap
@@ -155,13 +158,18 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
155
158
  self.velocity.get_value(),
156
159
  self.acceleration_time.get_value(),
157
160
  )
161
+
158
162
  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
- )
163
+ try:
164
+ timeout = (
165
+ abs((new_position - old_position) / velocity)
166
+ + 2 * acceleration_time
167
+ + DEFAULT_TIMEOUT
168
+ )
169
+ except ZeroDivisionError as error:
170
+ msg = "Mover has zero velocity"
171
+ raise ValueError(msg) from error
172
+
165
173
  move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
166
174
  async for current_position in observe_value(
167
175
  self.user_readback, done_status=move_status
@@ -0,0 +1,29 @@
1
+ import atexit
2
+ from pathlib import Path
3
+
4
+ from ophyd_async.epics.testing import TestingIOC
5
+
6
+ HERE = Path(__file__).absolute().parent
7
+
8
+
9
+ def start_ioc_subprocess(prefix: str, num_counters: int):
10
+ """Start an IOC subprocess with EPICS database for sample stage and sensor
11
+ with the same pv prefix
12
+ """
13
+ ioc = TestingIOC()
14
+ # Create X and Y motors
15
+ for suffix in ["X", "Y"]:
16
+ ioc.add_database(HERE / "mover.db", P=f"{prefix}STAGE:{suffix}:")
17
+ # Create a multichannel counter with num_counters
18
+ ioc.add_database(HERE / "multichannelcounter.db", P=f"{prefix}MCC:")
19
+ for i in range(1, num_counters + 1):
20
+ ioc.add_database(
21
+ HERE / "counter.db",
22
+ P=f"{prefix}MCC:",
23
+ CHANNEL=str(i),
24
+ X=f"{prefix}STAGE:X:",
25
+ Y=f"{prefix}STAGE:Y:",
26
+ )
27
+ # Start IOC and register it to be stopped at exit
28
+ ioc.start()
29
+ atexit.register(ioc.stop)
@@ -52,13 +52,19 @@ class Mover(StandardReadable, Movable, Stoppable):
52
52
  self.precision.get_value(),
53
53
  self.velocity.get_value(),
54
54
  )
55
- if timeout == CALCULATE_TIMEOUT:
56
- assert velocity > 0, "Mover has zero velocity"
57
- timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
55
+ if timeout is CALCULATE_TIMEOUT:
56
+ try:
57
+ timeout = (
58
+ abs((new_position - old_position) / velocity) + DEFAULT_TIMEOUT
59
+ )
60
+ except ZeroDivisionError as error:
61
+ msg = "Mover has zero velocity"
62
+ raise ValueError(msg) from error
63
+
58
64
  # Make an Event that will be set on completion, and a Status that will
59
65
  # error if not done in time
60
66
  done = asyncio.Event()
61
- done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout))
67
+ done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout)) # type: ignore
62
68
  # Wait for the value to set, but don't wait for put completion callback
63
69
  await self.setpoint.set(new_position, wait=False)
64
70
  async for current_position in observe_value(
@@ -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,49 @@
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):
26
18
  A = "Aaa"
27
19
  B = "Bbb"
28
20
  C = "Ccc"
29
21
 
30
22
 
31
- class ExampleTable(Table):
23
+ class EpicsTestSubsetEnum(SubsetEnum):
24
+ A = "Aaa"
25
+ B = "Bbb"
26
+
27
+
28
+ class EpicsTestTable(Table):
32
29
  bool: Array1D[np.bool_]
33
30
  int: Array1D[np.int32]
34
31
  float: Array1D[np.float64]
35
32
  str: Sequence[str]
36
- enum: Sequence[ExampleEnum]
33
+ enum: Sequence[EpicsTestEnum]
37
34
 
38
35
 
39
- class ExampleCaDevice(EpicsDevice):
36
+ class EpicsTestCaDevice(EpicsDevice):
40
37
  my_int: A[SignalRW[int], PvSuffix("int")]
41
38
  my_float: A[SignalRW[float], PvSuffix("float")]
39
+ float_prec_0: A[SignalRW[int], PvSuffix("float_prec_0")]
42
40
  my_str: A[SignalRW[str], PvSuffix("str")]
43
41
  longstr: A[SignalRW[str], PvSuffix("longstr")]
44
- longstr2: A[SignalRW[str], PvSuffix("longstr2")]
42
+ longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
45
43
  my_bool: A[SignalRW[bool], PvSuffix("bool")]
46
- enum: A[SignalRW[ExampleEnum], PvSuffix("enum")]
47
- enum2: A[SignalRW[ExampleEnum], PvSuffix("enum2")]
44
+ enum: A[SignalRW[EpicsTestEnum], PvSuffix("enum")]
45
+ enum2: A[SignalRW[EpicsTestEnum], PvSuffix("enum2")]
46
+ subset_enum: A[SignalRW[EpicsTestSubsetEnum], PvSuffix("subset_enum")]
48
47
  bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
49
48
  partialint: A[SignalRW[int], PvSuffix("partialint")]
50
49
  lessint: A[SignalRW[int], PvSuffix("lessint")]
@@ -56,52 +55,36 @@ class ExampleCaDevice(EpicsDevice):
56
55
  stra: A[SignalRW[Sequence[str]], PvSuffix("stra")]
57
56
 
58
57
 
59
- class ExamplePvaDevice(ExampleCaDevice): # pva can support all signal types that ca can
58
+ class EpicsTestPvaDevice(EpicsTestCaDevice):
59
+ # pva can support all signal types that ca can
60
60
  int8a: A[SignalRW[Array1D[np.int8]], PvSuffix("int8a")]
61
61
  uint16a: A[SignalRW[Array1D[np.uint16]], PvSuffix("uint16a")]
62
62
  uint32a: A[SignalRW[Array1D[np.uint32]], PvSuffix("uint32a")]
63
63
  int64a: A[SignalRW[Array1D[np.int64]], PvSuffix("int64a")]
64
64
  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
65
+ table: A[SignalRW[EpicsTestTable], PvSuffix("table")]
66
+ ntndarray: A[SignalR[np.ndarray], PvSuffix("ntndarray")]
67
+
68
+
69
+ class EpicsTestIocAndDevices:
70
+ def __init__(self):
71
+ self.prefix = generate_random_pv_prefix()
72
+ self.ioc = TestingIOC()
73
+ # Create supporting records and ExampleCaDevice
74
+ ca_prefix = f"{self.prefix}ca:"
75
+ self.ioc.add_database(CA_PVA_RECORDS, device=ca_prefix)
76
+ self.ca_device = EpicsTestCaDevice(f"ca://{ca_prefix}")
77
+ # Create supporting records and ExamplePvaDevice
78
+ pva_prefix = f"{self.prefix}pva:"
79
+ self.ioc.add_database(CA_PVA_RECORDS, device=pva_prefix)
80
+ self.ioc.add_database(PVA_RECORDS, device=pva_prefix)
81
+ self.pva_device = EpicsTestPvaDevice(f"pva://{pva_prefix}")
82
+
83
+ def get_device(self, protocol: str) -> EpicsTestCaDevice | EpicsTestPvaDevice:
84
+ return getattr(self, f"{protocol}_device")
85
+
86
+ def get_signal(self, protocol: str, name: str) -> SignalRW:
87
+ return getattr(self.get_device(protocol), name)
88
+
89
+ def get_pv(self, protocol: str, name: str) -> str:
90
+ return f"{protocol}://{self.prefix}{protocol}:{name}"
@@ -5,50 +5,28 @@ 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:
14
10
  return "".join(random.choice(string.ascii_lowercase) for _ in range(12)) + ":"
15
11
 
16
12
 
17
13
  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
28
-
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)
14
+ def __init__(self):
15
+ self._db_macros: list[tuple[Path, dict[str, str]]] = []
16
+ self.output = ""
37
17
 
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())
18
+ def add_database(self, db: Path | str, /, **macros: str):
19
+ self._db_macros.append((Path(db), macros))
41
20
 
42
- def start_ioc(self):
21
+ def start(self):
43
22
  ioc_args = [
44
23
  sys.executable,
45
24
  "-m",
46
25
  "epicscorelibs.ioc",
47
26
  ]
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)]
27
+ for db, macros in self._db_macros:
28
+ macro_str = ",".join(f"{k}={v}" for k, v in macros.items())
29
+ ioc_args += ["-m", macro_str, "-d", str(db)]
52
30
  self._process = subprocess.Popen(
53
31
  ioc_args,
54
32
  stdin=subprocess.PIPE,
@@ -56,23 +34,17 @@ class TestingIOC:
56
34
  stderr=subprocess.STDOUT,
57
35
  universal_newlines=True,
58
36
  )
37
+ assert self._process.stdout # noqa: S101 # this is to make Pylance happy
59
38
  start_time = time.monotonic()
60
- while "iocRun: All initialization complete" not in (
61
- self._process.stdout.readline().strip() # type: ignore
62
- ):
39
+ while "iocRun: All initialization complete" not in self.output:
63
40
  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")
41
+ self.stop()
42
+ raise TimeoutError(f"IOC did not start in time:\n{self.output}")
43
+ self.output += self._process.stdout.readline()
70
44
 
71
- def stop_ioc(self):
72
- # close backend caches before the event loop
73
- purge_channel_caches()
45
+ def stop(self):
74
46
  try:
75
- print(self._process.communicate("exit()")[0])
47
+ self.output += self._process.communicate("exit()")[0]
76
48
  except ValueError:
77
49
  # Someone else already called communicate
78
50
  pass
@@ -96,6 +96,14 @@ 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
+
99
107
  record(waveform, "$(device)uint8a") {
100
108
  field(NELM, "3")
101
109
  field(FTVL, "UCHAR")
@@ -23,7 +23,6 @@ from ._trigger import (
23
23
  StaticPcompTriggerLogic,
24
24
  StaticSeqTableTriggerLogic,
25
25
  )
26
- from ._utils import phase_sorter
27
26
  from ._writer import PandaHDFWriter
28
27
 
29
28
  __all__ = [
@@ -47,5 +46,4 @@ __all__ = [
47
46
  "SeqTableInfo",
48
47
  "StaticPcompTriggerLogic",
49
48
  "StaticSeqTableTriggerLogic",
50
- "phase_sorter",
51
49
  ]
@@ -18,10 +18,15 @@ class PandaPcapController(DetectorController):
18
18
  return 0.000000008
19
19
 
20
20
  async def prepare(self, trigger_info: TriggerInfo):
21
- assert trigger_info.trigger in (
21
+ if trigger_info.trigger not in (
22
22
  DetectorTrigger.CONSTANT_GATE,
23
23
  DetectorTrigger.VARIABLE_GATE,
24
- ), "Only constant_gate and variable_gate triggering is supported on the PandA"
24
+ ):
25
+ msg = (
26
+ "Only constant_gate and variable_gate triggering is supported on "
27
+ "the PandA",
28
+ )
29
+ raise TypeError(msg)
25
30
 
26
31
  async def arm(self):
27
32
  self._arm_status = self.pcap.arm.set(True)
@@ -12,7 +12,9 @@ from ._writer import PandaHDFWriter
12
12
  MINIMUM_PANDA_IOC = "0.11.4"
13
13
 
14
14
 
15
- class HDFPanda(CommonPandaBlocks, StandardDetector):
15
+ class HDFPanda(
16
+ CommonPandaBlocks, StandardDetector[PandaPcapController, PandaHDFWriter]
17
+ ):
16
18
  def __init__(
17
19
  self,
18
20
  prefix: str,
@@ -83,5 +83,8 @@ class SeqTable(Table):
83
83
  """
84
84
 
85
85
  first_length = len(self)
86
- assert first_length <= 4096, f"Length {first_length} is too long"
86
+ max_length = 4096
87
+ if first_length > max_length:
88
+ msg = f"Length {first_length} is too long"
89
+ raise ValueError(msg)
87
90
  return self
@@ -5,6 +5,14 @@ from ._fly import (
5
5
  time_resolved_fly_and_collect_with_static_seq_table,
6
6
  )
7
7
  from ._nd_attributes import setup_ndattributes, setup_ndstats_sum
8
+ from ._panda import apply_panda_settings
9
+ from ._settings import (
10
+ apply_settings,
11
+ apply_settings_if_different,
12
+ get_current_settings,
13
+ retrieve_settings,
14
+ store_settings,
15
+ )
8
16
 
9
17
  __all__ = [
10
18
  "fly_and_collect",
@@ -13,4 +21,10 @@ __all__ = [
13
21
  "ensure_connected",
14
22
  "setup_ndattributes",
15
23
  "setup_ndstats_sum",
24
+ "apply_panda_settings",
25
+ "apply_settings",
26
+ "apply_settings_if_different",
27
+ "get_current_settings",
28
+ "retrieve_settings",
29
+ "store_settings",
16
30
  ]
@@ -1,10 +1,11 @@
1
- from collections.abc import Awaitable
2
-
3
- import bluesky.plan_stubs as bps
1
+ from bluesky.utils import plan
4
2
 
5
3
  from ophyd_async.core import DEFAULT_TIMEOUT, Device, LazyMock, wait_for_connection
6
4
 
5
+ from ._wait_for_awaitable import wait_for_awaitable
6
+
7
7
 
8
+ @plan
8
9
  def ensure_connected(
9
10
  *devices: Device,
10
11
  mock: bool | LazyMock = False,
@@ -17,17 +18,10 @@ def ensure_connected(
17
18
  }
18
19
  if non_unique:
19
20
  raise ValueError(f"Devices do not have unique names {non_unique}")
20
-
21
- def connect_devices() -> Awaitable[None]:
22
- coros = {
23
- device.name: device.connect(
24
- mock=mock, timeout=timeout, force_reconnect=force_reconnect
25
- )
26
- for device in devices
27
- }
28
- return wait_for_connection(**coros)
29
-
30
- (connect_task,) = yield from bps.wait_for([connect_devices])
31
-
32
- if connect_task and connect_task.exception() is not None:
33
- raise connect_task.exception()
21
+ coros = {
22
+ device.name: device.connect(
23
+ mock=mock, timeout=timeout, force_reconnect=force_reconnect
24
+ )
25
+ for device in devices
26
+ }
27
+ yield from wait_for_awaitable(wait_for_connection(**coros))
@@ -58,7 +58,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
58
58
  if not detectors:
59
59
  raise ValueError("No detectors provided. There must be at least one.")
60
60
 
61
- deadtime = max(det.controller.get_deadtime(exposure) for det in detectors)
61
+ deadtime = max(det._controller.get_deadtime(exposure) for det in detectors) # noqa: SLF001
62
62
 
63
63
  trigger_info = TriggerInfo(
64
64
  number_of_triggers=number_of_frames * repeats,
@@ -50,11 +50,13 @@ def setup_ndattributes(
50
50
 
51
51
 
52
52
  def setup_ndstats_sum(detector: Device):
53
- hdf = getattr(detector, "hdf", None)
54
- assert isinstance(hdf, NDFileHDFIO), (
55
- f"Expected {detector.name} to have 'hdf' attribute that is an NDFilHDFIO, "
56
- f"got {hdf}"
57
- )
53
+ hdf = getattr(detector, "fileio", None)
54
+ if not isinstance(hdf, NDFileHDFIO):
55
+ msg = (
56
+ f"Expected {detector.name} to have 'fileio' attribute that is an "
57
+ f"NDFileHDFIO, got {hdf}"
58
+ )
59
+ raise TypeError(msg)
58
60
  yield from (
59
61
  setup_ndattributes(
60
62
  hdf,
@@ -0,0 +1,13 @@
1
+ from bluesky.utils import MsgGenerator, plan
2
+
3
+ from ophyd_async.core import Settings
4
+ from ophyd_async.fastcs import panda
5
+
6
+ from ._settings import apply_settings
7
+
8
+
9
+ @plan
10
+ def apply_panda_settings(settings: Settings[panda.HDFPanda]) -> MsgGenerator[None]:
11
+ units, others = settings.partition(lambda signal: signal.name.endswith("_units"))
12
+ yield from apply_settings(units)
13
+ yield from apply_settings(others)