ophyd-async 0.8.0a6__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +15 -46
  3. ophyd_async/core/_detector.py +68 -44
  4. ophyd_async/core/_device.py +120 -79
  5. ophyd_async/core/_device_filler.py +17 -8
  6. ophyd_async/core/_flyer.py +2 -2
  7. ophyd_async/core/_protocol.py +0 -28
  8. ophyd_async/core/_readable.py +30 -23
  9. ophyd_async/core/_settings.py +104 -0
  10. ophyd_async/core/_signal.py +91 -151
  11. ophyd_async/core/_signal_backend.py +4 -1
  12. ophyd_async/core/_soft_signal_backend.py +2 -1
  13. ophyd_async/core/_table.py +18 -10
  14. ophyd_async/core/_utils.py +30 -5
  15. ophyd_async/core/_yaml_settings.py +64 -0
  16. ophyd_async/epics/adandor/__init__.py +9 -0
  17. ophyd_async/epics/adandor/_andor.py +45 -0
  18. ophyd_async/epics/adandor/_andor_controller.py +49 -0
  19. ophyd_async/epics/adandor/_andor_io.py +36 -0
  20. ophyd_async/epics/adaravis/__init__.py +3 -1
  21. ophyd_async/epics/adaravis/_aravis.py +23 -37
  22. ophyd_async/epics/adaravis/_aravis_controller.py +21 -30
  23. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  24. ophyd_async/epics/adcore/__init__.py +15 -8
  25. ophyd_async/epics/adcore/_core_detector.py +41 -0
  26. ophyd_async/epics/adcore/_core_io.py +56 -31
  27. ophyd_async/epics/adcore/_core_logic.py +99 -86
  28. ophyd_async/epics/adcore/_core_writer.py +219 -0
  29. ophyd_async/epics/adcore/_hdf_writer.py +33 -59
  30. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  31. ophyd_async/epics/adcore/_single_trigger.py +5 -4
  32. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  33. ophyd_async/epics/adcore/_utils.py +37 -36
  34. ophyd_async/epics/adkinetix/_kinetix.py +29 -24
  35. ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
  36. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  37. ophyd_async/epics/adpilatus/__init__.py +2 -2
  38. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  39. ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
  40. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  41. ophyd_async/epics/adsimdetector/__init__.py +3 -3
  42. ophyd_async/epics/adsimdetector/_sim.py +33 -17
  43. ophyd_async/epics/advimba/_vimba.py +23 -23
  44. ophyd_async/epics/advimba/_vimba_controller.py +21 -35
  45. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  46. ophyd_async/epics/core/_aioca.py +52 -21
  47. ophyd_async/epics/core/_p4p.py +59 -16
  48. ophyd_async/epics/core/_pvi_connector.py +4 -2
  49. ophyd_async/epics/core/_signal.py +9 -2
  50. ophyd_async/epics/core/_util.py +10 -1
  51. ophyd_async/epics/eiger/_eiger_controller.py +4 -4
  52. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  53. ophyd_async/epics/motor.py +26 -15
  54. ophyd_async/epics/sim/_ioc.py +29 -0
  55. ophyd_async/epics/{demo → sim}/_mover.py +12 -6
  56. ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
  57. ophyd_async/epics/testing/__init__.py +14 -14
  58. ophyd_async/epics/testing/_example_ioc.py +53 -67
  59. ophyd_async/epics/testing/_utils.py +17 -45
  60. ophyd_async/epics/testing/test_records.db +22 -0
  61. ophyd_async/fastcs/core.py +2 -2
  62. ophyd_async/fastcs/panda/__init__.py +0 -2
  63. ophyd_async/fastcs/panda/_block.py +9 -9
  64. ophyd_async/fastcs/panda/_control.py +9 -4
  65. ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
  66. ophyd_async/fastcs/panda/_table.py +4 -1
  67. ophyd_async/fastcs/panda/_trigger.py +7 -7
  68. ophyd_async/plan_stubs/__init__.py +14 -0
  69. ophyd_async/plan_stubs/_ensure_connected.py +11 -17
  70. ophyd_async/plan_stubs/_fly.py +2 -2
  71. ophyd_async/plan_stubs/_nd_attributes.py +7 -5
  72. ophyd_async/plan_stubs/_panda.py +13 -0
  73. ophyd_async/plan_stubs/_settings.py +125 -0
  74. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  75. ophyd_async/sim/__init__.py +19 -0
  76. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
  77. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
  78. ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
  79. ophyd_async/tango/core/_signal.py +3 -1
  80. ophyd_async/tango/core/_tango_transport.py +13 -15
  81. ophyd_async/tango/{demo → sim}/_mover.py +5 -2
  82. ophyd_async/testing/__init__.py +52 -0
  83. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  84. ophyd_async/testing/_assert.py +176 -0
  85. ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
  86. ophyd_async/testing/_one_of_everything.py +126 -0
  87. ophyd_async/testing/_wait_for_pending.py +22 -0
  88. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +4 -2
  89. ophyd_async-0.9.0.dist-info/RECORD +129 -0
  90. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/WHEEL +1 -1
  91. ophyd_async/core/_device_save_loader.py +0 -274
  92. ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
  93. ophyd_async/fastcs/panda/_utils.py +0 -16
  94. ophyd_async/sim/demo/__init__.py +0 -19
  95. ophyd_async/sim/testing/__init__.py +0 -0
  96. ophyd_async-0.8.0a6.dist-info/RECORD +0 -116
  97. ophyd_async-0.8.0a6.dist-info/entry_points.txt +0 -2
  98. /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
  99. /ophyd_async/epics/{demo → sim}/mover.db +0 -0
  100. /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
  101. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
  102. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
  103. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
  104. /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
  105. /ophyd_async/tango/{demo → sim}/_counter.py +0 -0
  106. /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
  107. /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
  108. /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
  109. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
  110. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
@@ -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)
@@ -37,8 +37,8 @@ class Mover(StandardReadable, Movable, Stoppable):
37
37
 
38
38
  super().__init__(name=name)
39
39
 
40
- def set_name(self, name: str):
41
- super().set_name(name)
40
+ def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
41
+ super().set_name(name, child_name_separator=child_name_separator)
42
42
  # Readback should be named the same as its parent in read()
43
43
  self.readback.set_name(name)
44
44
 
@@ -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(
@@ -15,9 +15,9 @@ class EnergyMode(StrictEnum):
15
15
  """Energy mode for `Sensor`"""
16
16
 
17
17
  #: Low energy mode
18
- low = "Low Energy"
18
+ LOW = "Low Energy"
19
19
  #: High energy mode
20
- high = "High Energy"
20
+ HIGH = "High Energy"
21
21
 
22
22
 
23
23
  class Sensor(StandardReadable, EpicsDevice):
@@ -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,48 +1,50 @@
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):
26
- a = "Aaa"
27
- b = "Bbb"
28
- c = "Ccc"
17
+ class EpicsTestEnum(StrictEnum):
18
+ A = "Aaa"
19
+ B = "Bbb"
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")]
41
+ longstr: A[SignalRW[str], PvSuffix("longstr")]
42
+ longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
43
43
  my_bool: A[SignalRW[bool], PvSuffix("bool")]
44
- enum: A[SignalRW[ExampleEnum], PvSuffix("enum")]
45
- 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")]
47
+ enum_str_fallback: A[SignalRW[str], PvSuffix("enum_str_fallback")]
46
48
  bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
47
49
  partialint: A[SignalRW[int], PvSuffix("partialint")]
48
50
  lessint: A[SignalRW[int], PvSuffix("lessint")]
@@ -54,52 +56,36 @@ class ExampleCaDevice(EpicsDevice):
54
56
  stra: A[SignalRW[Sequence[str]], PvSuffix("stra")]
55
57
 
56
58
 
57
- class ExamplePvaDevice(ExampleCaDevice): # pva can support all signal types that ca can
59
+ class EpicsTestPvaDevice(EpicsTestCaDevice):
60
+ # pva can support all signal types that ca can
58
61
  int8a: A[SignalRW[Array1D[np.int8]], PvSuffix("int8a")]
59
62
  uint16a: A[SignalRW[Array1D[np.uint16]], PvSuffix("uint16a")]
60
63
  uint32a: A[SignalRW[Array1D[np.uint32]], PvSuffix("uint32a")]
61
64
  int64a: A[SignalRW[Array1D[np.int64]], PvSuffix("int64a")]
62
65
  uint64a: A[SignalRW[Array1D[np.uint64]], PvSuffix("uint64a")]
63
- table: A[SignalRW[ExampleTable], PvSuffix("table")]
64
- ntndarray_data: A[SignalRW[Array1D[np.int64]], PvSuffix("ntndarray:data")]
65
-
66
-
67
- async def connect_example_device(
68
- ioc: TestingIOC, protocol: Literal["ca", "pva"]
69
- ) -> ExamplePvaDevice | ExampleCaDevice:
70
- """Helper function to return a connected example device.
71
-
72
- Parameters
73
- ----------
74
-
75
- ioc: TestingIOC
76
- TestingIOC configured to provide the records needed for the device
77
-
78
- protocol: Literal["ca", "pva"]
79
- The transport protocol of the device
80
-
81
- Returns
82
- -------
83
- ExamplePvaDevice | ExampleCaDevice
84
- a connected EpicsDevice with signals of many EPICS record types
85
- """
86
- device_cls = ExamplePvaDevice if protocol == "pva" else ExampleCaDevice
87
- device = device_cls(f"{protocol}://{ioc.prefix_for(device_cls)}")
88
- await device.connect()
89
- return device
90
-
91
-
92
- def get_example_ioc() -> TestingIOC:
93
- """Get TestingIOC instance with the example databases loaded.
94
-
95
- Returns
96
- -------
97
- TestingIOC
98
- instance with test_records.db loaded for ExampleCaDevice and
99
- test_records.db and test_records_pva.db loaded for ExamplePvaDevice.
100
- """
101
- ioc = TestingIOC()
102
- ioc.database_for(PVA_RECORDS, ExamplePvaDevice)
103
- ioc.database_for(CA_PVA_RECORDS, ExamplePvaDevice)
104
- ioc.database_for(CA_PVA_RECORDS, ExampleCaDevice)
105
- return ioc
66
+ table: A[SignalRW[EpicsTestTable], PvSuffix("table")]
67
+ ntndarray: A[SignalR[np.ndarray], PvSuffix("ntndarray")]
68
+
69
+
70
+ class EpicsTestIocAndDevices:
71
+ def __init__(self):
72
+ self.prefix = generate_random_pv_prefix()
73
+ self.ioc = TestingIOC()
74
+ # Create supporting records and ExampleCaDevice
75
+ ca_prefix = f"{self.prefix}ca:"
76
+ self.ioc.add_database(CA_PVA_RECORDS, device=ca_prefix)
77
+ self.ca_device = EpicsTestCaDevice(f"ca://{ca_prefix}")
78
+ # Create supporting records and ExamplePvaDevice
79
+ pva_prefix = f"{self.prefix}pva:"
80
+ self.ioc.add_database(CA_PVA_RECORDS, device=pva_prefix)
81
+ self.ioc.add_database(PVA_RECORDS, device=pva_prefix)
82
+ self.pva_device = EpicsTestPvaDevice(f"pva://{pva_prefix}")
83
+
84
+ def get_device(self, protocol: str) -> EpicsTestCaDevice | EpicsTestPvaDevice:
85
+ return getattr(self, f"{protocol}_device")
86
+
87
+ def get_signal(self, protocol: str, name: str) -> SignalRW:
88
+ return getattr(self.get_device(protocol), name)
89
+
90
+ def get_pv(self, protocol: str, name: str) -> str:
91
+ 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,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")
@@ -150,3 +166,9 @@ record(lsi, "$(device)longstr2") {
150
166
  field(INP, {const:"a string that is just longer than forty characters"})
151
167
  field(PINI, "YES")
152
168
  }
169
+
170
+ record(calc, "$(device)ticking") {
171
+ field(INPA, "$(device)ticking")
172
+ field(CALC, "A+1")
173
+ field(SCAN, ".1 second")
174
+ }
@@ -2,8 +2,8 @@ from ophyd_async.core import Device, DeviceConnector
2
2
  from ophyd_async.epics.core import PviDeviceConnector
3
3
 
4
4
 
5
- def fastcs_connector(device: Device, uri: str) -> DeviceConnector:
5
+ def fastcs_connector(device: Device, uri: str, error_hint: str = "") -> DeviceConnector:
6
6
  # TODO: add Tango support based on uri scheme
7
- connector = PviDeviceConnector(uri)
7
+ connector = PviDeviceConnector(uri, error_hint)
8
8
  connector.create_children_from_annotations(device)
9
9
  return connector
@@ -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
  ]
@@ -36,14 +36,14 @@ class PulseBlock(Device):
36
36
 
37
37
 
38
38
  class PcompDirection(StrictEnum):
39
- positive = "Positive"
40
- negative = "Negative"
41
- either = "Either"
39
+ POSITIVE = "Positive"
40
+ NEGATIVE = "Negative"
41
+ EITHER = "Either"
42
42
 
43
43
 
44
44
  class BitMux(SubsetEnum):
45
- zero = "ZERO"
46
- one = "ONE"
45
+ ZERO = "ZERO"
46
+ ONE = "ONE"
47
47
 
48
48
 
49
49
  class PcompBlock(Device):
@@ -57,10 +57,10 @@ class PcompBlock(Device):
57
57
 
58
58
 
59
59
  class TimeUnits(StrictEnum):
60
- min = "min"
61
- s = "s"
62
- ms = "ms"
63
- us = "us"
60
+ MIN = "min"
61
+ S = "s"
62
+ MS = "ms"
63
+ US = "us"
64
64
 
65
65
 
66
66
  class SeqBlock(Device):
@@ -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 (
22
- DetectorTrigger.constant_gate,
23
- DetectorTrigger.variable_gate,
24
- ), "Only constant_gate and variable_gate triggering is supported on the PandA"
21
+ if trigger_info.trigger not in (
22
+ DetectorTrigger.CONSTANT_GATE,
23
+ DetectorTrigger.VARIABLE_GATE,
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)
@@ -9,8 +9,12 @@ from ._block import CommonPandaBlocks
9
9
  from ._control import PandaPcapController
10
10
  from ._writer import PandaHDFWriter
11
11
 
12
+ MINIMUM_PANDA_IOC = "0.11.4"
12
13
 
13
- class HDFPanda(CommonPandaBlocks, StandardDetector):
14
+
15
+ class HDFPanda(
16
+ CommonPandaBlocks, StandardDetector[PandaPcapController, PandaHDFWriter]
17
+ ):
14
18
  def __init__(
15
19
  self,
16
20
  prefix: str,
@@ -18,8 +22,9 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
18
22
  config_sigs: Sequence[SignalR] = (),
19
23
  name: str = "",
20
24
  ):
25
+ error_hint = f"Is PandABlocks-ioc at least version {MINIMUM_PANDA_IOC}?"
21
26
  # This has to be first so we make self.pcap
22
- connector = fastcs_connector(self, prefix)
27
+ connector = fastcs_connector(self, prefix, error_hint)
23
28
  controller = PandaPcapController(pcap=self.pcap)
24
29
  writer = PandaHDFWriter(
25
30
  path_provider=path_provider,
@@ -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
@@ -20,8 +20,8 @@ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
20
20
 
21
21
  async def prepare(self, value: SeqTableInfo):
22
22
  await asyncio.gather(
23
- self.seq.prescale_units.set(TimeUnits.us),
24
- self.seq.enable.set(BitMux.zero),
23
+ self.seq.prescale_units.set(TimeUnits.US),
24
+ self.seq.enable.set(BitMux.ZERO),
25
25
  )
26
26
  await asyncio.gather(
27
27
  self.seq.prescale.set(value.prescale_as_us),
@@ -30,14 +30,14 @@ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
30
30
  )
31
31
 
32
32
  async def kickoff(self) -> None:
33
- await self.seq.enable.set(BitMux.one)
33
+ await self.seq.enable.set(BitMux.ONE)
34
34
  await wait_for_value(self.seq.active, True, timeout=1)
35
35
 
36
36
  async def complete(self) -> None:
37
37
  await wait_for_value(self.seq.active, False, timeout=None)
38
38
 
39
39
  async def stop(self):
40
- await self.seq.enable.set(BitMux.zero)
40
+ await self.seq.enable.set(BitMux.ZERO)
41
41
  await wait_for_value(self.seq.active, False, timeout=1)
42
42
 
43
43
 
@@ -68,7 +68,7 @@ class StaticPcompTriggerLogic(FlyerController[PcompInfo]):
68
68
  self.pcomp = pcomp
69
69
 
70
70
  async def prepare(self, value: PcompInfo):
71
- await self.pcomp.enable.set(BitMux.zero)
71
+ await self.pcomp.enable.set(BitMux.ZERO)
72
72
  await asyncio.gather(
73
73
  self.pcomp.start.set(value.start_postion),
74
74
  self.pcomp.width.set(value.pulse_width),
@@ -78,12 +78,12 @@ class StaticPcompTriggerLogic(FlyerController[PcompInfo]):
78
78
  )
79
79
 
80
80
  async def kickoff(self) -> None:
81
- await self.pcomp.enable.set(BitMux.one)
81
+ await self.pcomp.enable.set(BitMux.ONE)
82
82
  await wait_for_value(self.pcomp.active, True, timeout=1)
83
83
 
84
84
  async def complete(self, timeout: float | None = None) -> None:
85
85
  await wait_for_value(self.pcomp.active, False, timeout=timeout)
86
86
 
87
87
  async def stop(self):
88
- await self.pcomp.enable.set(BitMux.zero)
88
+ await self.pcomp.enable.set(BitMux.ZERO)
89
89
  await wait_for_value(self.pcomp.active, False, timeout=1)
@@ -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,11 +58,11 @@ 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,
65
- trigger=DetectorTrigger.constant_gate,
65
+ trigger=DetectorTrigger.CONSTANT_GATE,
66
66
  deadtime=deadtime,
67
67
  livetime=exposure,
68
68
  frame_timeout=frame_timeout,