ophyd-async 0.7.0__py3-none-any.whl → 0.8.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 (92) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +34 -9
  3. ophyd_async/core/_detector.py +5 -10
  4. ophyd_async/core/_device.py +170 -68
  5. ophyd_async/core/_device_filler.py +269 -0
  6. ophyd_async/core/_device_save_loader.py +6 -7
  7. ophyd_async/core/_mock_signal_backend.py +35 -40
  8. ophyd_async/core/_mock_signal_utils.py +25 -16
  9. ophyd_async/core/_protocol.py +28 -8
  10. ophyd_async/core/_readable.py +133 -134
  11. ophyd_async/core/_signal.py +219 -163
  12. ophyd_async/core/_signal_backend.py +131 -64
  13. ophyd_async/core/_soft_signal_backend.py +131 -194
  14. ophyd_async/core/_status.py +22 -6
  15. ophyd_async/core/_table.py +102 -100
  16. ophyd_async/core/_utils.py +143 -32
  17. ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
  18. ophyd_async/epics/adaravis/_aravis_io.py +8 -6
  19. ophyd_async/epics/adcore/_core_io.py +5 -7
  20. ophyd_async/epics/adcore/_core_logic.py +3 -1
  21. ophyd_async/epics/adcore/_hdf_writer.py +2 -2
  22. ophyd_async/epics/adcore/_single_trigger.py +6 -10
  23. ophyd_async/epics/adcore/_utils.py +15 -10
  24. ophyd_async/epics/adkinetix/__init__.py +2 -1
  25. ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
  26. ophyd_async/epics/adkinetix/_kinetix_io.py +4 -5
  27. ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
  28. ophyd_async/epics/adpilatus/_pilatus_io.py +3 -4
  29. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  30. ophyd_async/epics/advimba/__init__.py +4 -1
  31. ophyd_async/epics/advimba/_vimba_controller.py +6 -3
  32. ophyd_async/epics/advimba/_vimba_io.py +8 -9
  33. ophyd_async/epics/core/__init__.py +26 -0
  34. ophyd_async/epics/core/_aioca.py +323 -0
  35. ophyd_async/epics/core/_epics_connector.py +53 -0
  36. ophyd_async/epics/core/_epics_device.py +13 -0
  37. ophyd_async/epics/core/_p4p.py +383 -0
  38. ophyd_async/epics/core/_pvi_connector.py +91 -0
  39. ophyd_async/epics/core/_signal.py +171 -0
  40. ophyd_async/epics/core/_util.py +61 -0
  41. ophyd_async/epics/demo/_mover.py +4 -5
  42. ophyd_async/epics/demo/_sensor.py +14 -13
  43. ophyd_async/epics/eiger/_eiger.py +1 -2
  44. ophyd_async/epics/eiger/_eiger_controller.py +7 -2
  45. ophyd_async/epics/eiger/_eiger_io.py +3 -5
  46. ophyd_async/epics/eiger/_odin_io.py +5 -5
  47. ophyd_async/epics/motor.py +4 -5
  48. ophyd_async/epics/signal.py +11 -0
  49. ophyd_async/epics/testing/__init__.py +24 -0
  50. ophyd_async/epics/testing/_example_ioc.py +105 -0
  51. ophyd_async/epics/testing/_utils.py +78 -0
  52. ophyd_async/epics/testing/test_records.db +152 -0
  53. ophyd_async/epics/testing/test_records_pva.db +177 -0
  54. ophyd_async/fastcs/core.py +9 -0
  55. ophyd_async/fastcs/panda/__init__.py +4 -4
  56. ophyd_async/fastcs/panda/_block.py +18 -13
  57. ophyd_async/fastcs/panda/_control.py +3 -5
  58. ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
  59. ophyd_async/fastcs/panda/_table.py +30 -52
  60. ophyd_async/fastcs/panda/_trigger.py +8 -8
  61. ophyd_async/fastcs/panda/_writer.py +2 -5
  62. ophyd_async/plan_stubs/_ensure_connected.py +20 -13
  63. ophyd_async/plan_stubs/_fly.py +2 -2
  64. ophyd_async/plan_stubs/_nd_attributes.py +5 -4
  65. ophyd_async/py.typed +0 -0
  66. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
  67. ophyd_async/sim/demo/_sim_motor.py +3 -4
  68. ophyd_async/tango/__init__.py +0 -45
  69. ophyd_async/tango/{signal → core}/__init__.py +9 -6
  70. ophyd_async/tango/core/_base_device.py +132 -0
  71. ophyd_async/tango/{signal → core}/_signal.py +42 -53
  72. ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
  73. ophyd_async/tango/{signal → core}/_tango_transport.py +38 -40
  74. ophyd_async/tango/demo/_counter.py +12 -23
  75. ophyd_async/tango/demo/_mover.py +13 -13
  76. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/METADATA +52 -55
  77. ophyd_async-0.8.0.dist-info/RECORD +116 -0
  78. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/WHEEL +1 -1
  79. ophyd_async/epics/pvi/__init__.py +0 -3
  80. ophyd_async/epics/pvi/_pvi.py +0 -338
  81. ophyd_async/epics/signal/__init__.py +0 -21
  82. ophyd_async/epics/signal/_aioca.py +0 -378
  83. ophyd_async/epics/signal/_common.py +0 -57
  84. ophyd_async/epics/signal/_epics_transport.py +0 -34
  85. ophyd_async/epics/signal/_p4p.py +0 -518
  86. ophyd_async/epics/signal/_signal.py +0 -114
  87. ophyd_async/tango/base_devices/__init__.py +0 -4
  88. ophyd_async/tango/base_devices/_base_device.py +0 -225
  89. ophyd_async-0.7.0.dist-info/RECORD +0 -108
  90. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/LICENSE +0 -0
  91. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/entry_points.txt +0 -0
  92. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,61 @@
1
+ from collections.abc import Sequence
2
+ from typing import Any, get_args, get_origin
3
+
4
+ import numpy as np
5
+
6
+ from ophyd_async.core import (
7
+ SignalBackend,
8
+ SignalDatatypeT,
9
+ SubsetEnum,
10
+ get_dtype,
11
+ get_enum_cls,
12
+ )
13
+
14
+
15
+ def get_supported_values(
16
+ pv: str,
17
+ datatype: type,
18
+ pv_choices: Sequence[str],
19
+ ) -> dict[str, str]:
20
+ enum_cls = get_enum_cls(datatype)
21
+ if not enum_cls:
22
+ raise TypeError(f"{datatype} is not an Enum")
23
+ choices = [v.value for v in enum_cls]
24
+ error_msg = f"{pv} has choices {pv_choices}, but {datatype} requested {choices} "
25
+ if issubclass(enum_cls, SubsetEnum):
26
+ if not set(choices).issubset(pv_choices):
27
+ raise TypeError(error_msg + "to be a subset of them.")
28
+ else:
29
+ if set(choices) != set(pv_choices):
30
+ raise TypeError(error_msg + "to be strictly equal to them.")
31
+
32
+ # Take order from the pv choices
33
+ supported_values = {x: x for x in pv_choices}
34
+ # But override those that we specify via the datatype
35
+ for v in enum_cls:
36
+ supported_values[v.value] = v
37
+ return supported_values
38
+
39
+
40
+ def format_datatype(datatype: Any) -> str:
41
+ if get_origin(datatype) is np.ndarray and get_args(datatype)[0] == tuple[int]:
42
+ dtype = get_dtype(datatype)
43
+ return f"Array1D[np.{dtype.name}]"
44
+ elif get_origin(datatype) is Sequence:
45
+ return f"Sequence[{get_args(datatype)[0].__name__}]"
46
+ elif isinstance(datatype, type):
47
+ return datatype.__name__
48
+ else:
49
+ return str(datatype)
50
+
51
+
52
+ class EpicsSignalBackend(SignalBackend[SignalDatatypeT]):
53
+ def __init__(
54
+ self,
55
+ datatype: type[SignalDatatypeT] | None,
56
+ read_pv: str = "",
57
+ write_pv: str = "",
58
+ ):
59
+ self.read_pv = read_pv
60
+ self.write_pv = write_pv
61
+ super().__init__(datatype)
@@ -8,15 +8,14 @@ from ophyd_async.core import (
8
8
  DEFAULT_TIMEOUT,
9
9
  AsyncStatus,
10
10
  CalculatableTimeout,
11
- ConfigSignal,
12
11
  Device,
13
- HintedSignal,
14
12
  StandardReadable,
15
13
  WatchableAsyncStatus,
16
14
  WatcherUpdate,
17
15
  observe_value,
18
16
  )
19
- from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x
17
+ from ophyd_async.core import StandardReadableFormat as Format
18
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
20
19
 
21
20
 
22
21
  class Mover(StandardReadable, Movable, Stoppable):
@@ -24,9 +23,9 @@ class Mover(StandardReadable, Movable, Stoppable):
24
23
 
25
24
  def __init__(self, prefix: str, name="") -> None:
26
25
  # Define some signals
27
- with self.add_children_as_readables(HintedSignal):
26
+ with self.add_children_as_readables(Format.HINTED_SIGNAL):
28
27
  self.readback = epics_signal_r(float, prefix + "Readback")
29
- with self.add_children_as_readables(ConfigSignal):
28
+ with self.add_children_as_readables(Format.CONFIG_SIGNAL):
30
29
  self.velocity = epics_signal_rw(float, prefix + "Velocity")
31
30
  self.units = epics_signal_r(str, prefix + "Readback.EGU")
32
31
  self.setpoint = epics_signal_rw(float, prefix + "Setpoint")
@@ -1,10 +1,17 @@
1
- from enum import Enum
1
+ from typing import Annotated as A
2
2
 
3
- from ophyd_async.core import ConfigSignal, DeviceVector, HintedSignal, StandardReadable
4
- from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
3
+ from ophyd_async.core import (
4
+ DeviceVector,
5
+ SignalR,
6
+ SignalRW,
7
+ StandardReadable,
8
+ StrictEnum,
9
+ )
10
+ from ophyd_async.core import StandardReadableFormat as Format
11
+ from ophyd_async.epics.core import EpicsDevice, PvSuffix
5
12
 
6
13
 
7
- class EnergyMode(str, Enum):
14
+ class EnergyMode(StrictEnum):
8
15
  """Energy mode for `Sensor`"""
9
16
 
10
17
  #: Low energy mode
@@ -13,17 +20,11 @@ class EnergyMode(str, Enum):
13
20
  high = "High Energy"
14
21
 
15
22
 
16
- class Sensor(StandardReadable):
23
+ class Sensor(StandardReadable, EpicsDevice):
17
24
  """A demo sensor that produces a scalar value based on X and Y Movers"""
18
25
 
19
- def __init__(self, prefix: str, name="") -> None:
20
- # Define some signals
21
- with self.add_children_as_readables(HintedSignal):
22
- self.value = epics_signal_r(float, prefix + "Value")
23
- with self.add_children_as_readables(ConfigSignal):
24
- self.mode = epics_signal_rw(EnergyMode, prefix + "Mode")
25
-
26
- super().__init__(name=name)
26
+ value: A[SignalR[float], PvSuffix("Value"), Format.HINTED_SIGNAL]
27
+ mode: A[SignalRW[EnergyMode], PvSuffix("Mode"), Format.CONFIG_SIGNAL]
27
28
 
28
29
 
29
30
  class SensorGroup(StandardReadable):
@@ -1,7 +1,6 @@
1
1
  from pydantic import Field
2
2
 
3
- from ophyd_async.core import AsyncStatus, PathProvider, StandardDetector
4
- from ophyd_async.core._detector import TriggerInfo
3
+ from ophyd_async.core import AsyncStatus, PathProvider, StandardDetector, TriggerInfo
5
4
 
6
5
  from ._eiger_controller import EigerController
7
6
  from ._eiger_io import EigerDriverIO
@@ -4,9 +4,9 @@ from ophyd_async.core import (
4
4
  DEFAULT_TIMEOUT,
5
5
  DetectorController,
6
6
  DetectorTrigger,
7
+ TriggerInfo,
7
8
  set_and_wait_for_other_value,
8
9
  )
9
- from ophyd_async.core._detector import TriggerInfo
10
10
 
11
11
  from ._eiger_io import EigerDriverIO, EigerTriggerMode
12
12
 
@@ -55,7 +55,12 @@ class EigerController(DetectorController):
55
55
  async def arm(self):
56
56
  # TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
57
57
  self._arm_status = set_and_wait_for_other_value(
58
- self._drv.arm, 1, self._drv.state, "ready", timeout=DEFAULT_TIMEOUT
58
+ self._drv.arm,
59
+ 1,
60
+ self._drv.state,
61
+ "ready",
62
+ timeout=DEFAULT_TIMEOUT,
63
+ wait_for_set_completion=False,
59
64
  )
60
65
 
61
66
  async def wait_for_idle(self):
@@ -1,10 +1,8 @@
1
- from enum import Enum
1
+ from ophyd_async.core import Device, StrictEnum
2
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw_rbv, epics_signal_w
2
3
 
3
- from ophyd_async.core import Device
4
- from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw_rbv, epics_signal_w
5
4
 
6
-
7
- class EigerTriggerMode(str, Enum):
5
+ class EigerTriggerMode(StrictEnum):
8
6
  internal = "ints"
9
7
  edge = "exts"
10
8
  gate = "exte"
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
2
  from collections.abc import AsyncGenerator, AsyncIterator
3
- from enum import Enum
4
3
 
5
4
  from bluesky.protocols import StreamAsset
6
5
  from event_model import DataKey
@@ -12,17 +11,18 @@ from ophyd_async.core import (
12
11
  DeviceVector,
13
12
  NameProvider,
14
13
  PathProvider,
14
+ StrictEnum,
15
15
  observe_value,
16
16
  set_and_wait_for_value,
17
17
  )
18
- from ophyd_async.epics.signal import (
18
+ from ophyd_async.epics.core import (
19
19
  epics_signal_r,
20
20
  epics_signal_rw,
21
21
  epics_signal_rw_rbv,
22
22
  )
23
23
 
24
24
 
25
- class Writing(str, Enum):
25
+ class Writing(StrictEnum):
26
26
  ON = "ON"
27
27
  OFF = "OFF"
28
28
 
@@ -101,10 +101,10 @@ class OdinWriter(DetectorWriter):
101
101
  return {
102
102
  "data": DataKey(
103
103
  source=self._drv.file_name.source,
104
- shape=data_shape,
104
+ shape=list(data_shape),
105
105
  dtype="array",
106
106
  # TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529
107
- dtype_numpy="<u2", # type: ignore
107
+ dtype_numpy="<u2",
108
108
  external="STREAM:",
109
109
  )
110
110
  }
@@ -14,14 +14,13 @@ from ophyd_async.core import (
14
14
  DEFAULT_TIMEOUT,
15
15
  AsyncStatus,
16
16
  CalculatableTimeout,
17
- ConfigSignal,
18
- HintedSignal,
19
17
  StandardReadable,
20
18
  WatchableAsyncStatus,
21
19
  WatcherUpdate,
22
20
  observe_value,
23
21
  )
24
- from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x
22
+ from ophyd_async.core import StandardReadableFormat as Format
23
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
25
24
 
26
25
 
27
26
  class MotorLimitsException(Exception):
@@ -61,11 +60,11 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
61
60
 
62
61
  def __init__(self, prefix: str, name="") -> None:
63
62
  # Define some signals
64
- with self.add_children_as_readables(ConfigSignal):
63
+ with self.add_children_as_readables(Format.CONFIG_SIGNAL):
65
64
  self.motor_egu = epics_signal_r(str, prefix + ".EGU")
66
65
  self.velocity = epics_signal_rw(float, prefix + ".VELO")
67
66
 
68
- with self.add_children_as_readables(HintedSignal):
67
+ with self.add_children_as_readables(Format.HINTED_SIGNAL):
69
68
  self.user_readback = epics_signal_r(float, prefix + ".RBV")
70
69
 
71
70
  self.user_setpoint = epics_signal_rw(float, prefix + ".VAL")
@@ -0,0 +1,11 @@
1
+ # back compat
2
+ import warnings
3
+
4
+ from .core import * # noqa: F403
5
+
6
+ warnings.warn(
7
+ DeprecationWarning(
8
+ "Use `ophyd_async.epics.core` instead of `ophyd_async.epics.signal` and `pvi`"
9
+ ),
10
+ stacklevel=2,
11
+ )
@@ -0,0 +1,24 @@
1
+ from ._example_ioc import (
2
+ CA_PVA_RECORDS,
3
+ PVA_RECORDS,
4
+ ExampleCaDevice,
5
+ ExampleEnum,
6
+ ExamplePvaDevice,
7
+ ExampleTable,
8
+ connect_example_device,
9
+ get_example_ioc,
10
+ )
11
+ from ._utils import TestingIOC, generate_random_PV_prefix
12
+
13
+ __all__ = [
14
+ "CA_PVA_RECORDS",
15
+ "PVA_RECORDS",
16
+ "ExampleCaDevice",
17
+ "ExampleEnum",
18
+ "ExamplePvaDevice",
19
+ "ExampleTable",
20
+ "connect_example_device",
21
+ "get_example_ioc",
22
+ "TestingIOC",
23
+ "generate_random_PV_prefix",
24
+ ]
@@ -0,0 +1,105 @@
1
+ from collections.abc import Sequence
2
+ from pathlib import Path
3
+ from typing import Annotated as A
4
+ from typing import Literal
5
+
6
+ import numpy as np
7
+
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
+ )
18
+
19
+ from ._utils import TestingIOC
20
+
21
+ CA_PVA_RECORDS = str(Path(__file__).parent / "test_records.db")
22
+ PVA_RECORDS = str(Path(__file__).parent / "test_records_pva.db")
23
+
24
+
25
+ class ExampleEnum(StrictEnum):
26
+ a = "Aaa"
27
+ b = "Bbb"
28
+ c = "Ccc"
29
+
30
+
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]
37
+
38
+
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")]
43
+ my_bool: A[SignalRW[bool], PvSuffix("bool")]
44
+ enum: A[SignalRW[ExampleEnum], PvSuffix("enum")]
45
+ enum2: A[SignalRW[ExampleEnum], PvSuffix("enum2")]
46
+ bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
47
+ partialint: A[SignalRW[int], PvSuffix("partialint")]
48
+ lessint: A[SignalRW[int], PvSuffix("lessint")]
49
+ uint8a: A[SignalRW[Array1D[np.uint8]], PvSuffix("uint8a")]
50
+ int16a: A[SignalRW[Array1D[np.int16]], PvSuffix("int16a")]
51
+ int32a: A[SignalRW[Array1D[np.int32]], PvSuffix("int32a")]
52
+ float32a: A[SignalRW[Array1D[np.float32]], PvSuffix("float32a")]
53
+ float64a: A[SignalRW[Array1D[np.float64]], PvSuffix("float64a")]
54
+ stra: A[SignalRW[Sequence[str]], PvSuffix("stra")]
55
+
56
+
57
+ class ExamplePvaDevice(ExampleCaDevice): # pva can support all signal types that ca can
58
+ int8a: A[SignalRW[Array1D[np.int8]], PvSuffix("int8a")]
59
+ uint16a: A[SignalRW[Array1D[np.uint16]], PvSuffix("uint16a")]
60
+ uint32a: A[SignalRW[Array1D[np.uint32]], PvSuffix("uint32a")]
61
+ int64a: A[SignalRW[Array1D[np.int64]], PvSuffix("int64a")]
62
+ 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
@@ -0,0 +1,78 @@
1
+ import random
2
+ import string
3
+ import subprocess
4
+ import sys
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from aioca import purge_channel_caches
9
+
10
+ from ophyd_async.core import Device
11
+
12
+
13
+ def generate_random_PV_prefix() -> str:
14
+ return "".join(random.choice(string.ascii_lowercase) for _ in range(12)) + ":"
15
+
16
+
17
+ 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)
37
+
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())
41
+
42
+ def start_ioc(self):
43
+ ioc_args = [
44
+ sys.executable,
45
+ "-m",
46
+ "epicscorelibs.ioc",
47
+ ]
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)]
52
+ self._process = subprocess.Popen(
53
+ ioc_args,
54
+ stdin=subprocess.PIPE,
55
+ stdout=subprocess.PIPE,
56
+ stderr=subprocess.STDOUT,
57
+ universal_newlines=True,
58
+ )
59
+ start_time = time.monotonic()
60
+ while "iocRun: All initialization complete" not in (
61
+ self._process.stdout.readline().strip() # type: ignore
62
+ ):
63
+ 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")
70
+
71
+ def stop_ioc(self):
72
+ # close backend caches before the event loop
73
+ purge_channel_caches()
74
+ try:
75
+ print(self._process.communicate("exit()")[0])
76
+ except ValueError:
77
+ # Someone else already called communicate
78
+ pass
@@ -0,0 +1,152 @@
1
+ record(bo, "$(device)bool") {
2
+ field(ZNAM, "No")
3
+ field(ONAM, "Yes")
4
+ field(VAL, "1")
5
+ field(PINI, "YES")
6
+ }
7
+
8
+ record(bo, "$(device)bool_unnamed") {
9
+ field(VAL, "1")
10
+ field(PINI, "YES")
11
+ }
12
+
13
+ record(longout, "$(device)int") {
14
+ field(LLSV, "MAJOR") # LOLO is alarm
15
+ field(LSV, "MINOR") # LOW is warning
16
+ field(HSV, "MINOR") # HIGH is warning
17
+ field(HHSV, "MAJOR") # HIHI is alarm
18
+ field(HOPR, "100")
19
+ field(HIHI, "98")
20
+ field(HIGH, "96")
21
+ field(DRVH, "90")
22
+ field(DRVL, "10")
23
+ field(LOW, "5")
24
+ field(LOLO, "2")
25
+ field(LOPR, "0")
26
+ field(VAL, "42")
27
+ field(PINI, "YES")
28
+ }
29
+
30
+ record(longout, "$(device)partialint") {
31
+ field(LLSV, "MAJOR") # LOLO is alarm
32
+ field(HHSV, "MAJOR") # HIHI is alarm
33
+ field(HOPR, "100")
34
+ field(HIHI, "98")
35
+ field(DRVH, "90")
36
+ field(DRVL, "10")
37
+ field(LOLO, "2")
38
+ field(LOPR, "0")
39
+ field(VAL, "42")
40
+ field(PINI, "YES")
41
+ }
42
+
43
+ record(longout, "$(device)lessint") {
44
+ field(HSV, "MINOR") # LOW is warning
45
+ field(LSV, "MINOR") # HIGH is warning
46
+ field(HOPR, "100")
47
+ field(HIGH, "98")
48
+ field(LOW, "2")
49
+ field(LOPR, "0")
50
+ field(VAL, "42")
51
+ field(PINI, "YES")
52
+ }
53
+
54
+ record(ao, "$(device)float") {
55
+ field(PREC, "1")
56
+ field(EGU, "mm")
57
+ field(VAL, "3.141")
58
+ field(PINI, "YES")
59
+ }
60
+
61
+ record(ao, "$(device)float_prec_0") {
62
+ field(PREC, "0")
63
+ field(EGU, "mm")
64
+ field(VAL, "3")
65
+ field(PINI, "YES")
66
+ }
67
+
68
+ record(ao, "$(device)float_prec_1") {
69
+ field(PREC, "1")
70
+ field(EGU, "mm")
71
+ field(VAL, "3")
72
+ field(PINI, "YES")
73
+ }
74
+
75
+ record(stringout, "$(device)str") {
76
+ field(VAL, "hello")
77
+ field(PINI, "YES")
78
+ }
79
+
80
+ record(mbbo, "$(device)enum") {
81
+ field(ZRST, "Aaa")
82
+ field(ZRVL, "5")
83
+ field(ONST, "Bbb")
84
+ field(ONVL, "6")
85
+ field(TWST, "Ccc")
86
+ field(TWVL, "7")
87
+ field(VAL, "1")
88
+ field(PINI, "YES")
89
+ }
90
+
91
+ record(mbbo, "$(device)enum2") {
92
+ field(ZRST, "Aaa")
93
+ field(ONST, "Bbb")
94
+ field(TWST, "Ccc")
95
+ field(VAL, "1")
96
+ field(PINI, "YES")
97
+ }
98
+
99
+ record(waveform, "$(device)uint8a") {
100
+ field(NELM, "3")
101
+ field(FTVL, "UCHAR")
102
+ field(INP, {const:[0, 255]})
103
+ field(PINI, "YES")
104
+ }
105
+
106
+ record(waveform, "$(device)int16a") {
107
+ field(NELM, "3")
108
+ field(FTVL, "SHORT")
109
+ field(INP, {const:[-32768, 32767]})
110
+ field(PINI, "YES")
111
+ }
112
+
113
+ record(waveform, "$(device)int32a") {
114
+ field(NELM, "3")
115
+ field(FTVL, "LONG")
116
+ field(INP, {const:[-2147483648, 2147483647]})
117
+ field(PINI, "YES")
118
+ }
119
+
120
+ record(waveform, "$(device)float32a") {
121
+ field(NELM, "3")
122
+ field(FTVL, "FLOAT")
123
+ field(INP, {const:[0.000002, -123.123]})
124
+ field(PINI, "YES")
125
+ }
126
+
127
+ record(waveform, "$(device)float64a") {
128
+ field(NELM, "3")
129
+ field(FTVL, "DOUBLE")
130
+ field(INP, {const:[0.1, -12345678.123]})
131
+ field(PINI, "YES")
132
+ }
133
+
134
+ record(waveform, "$(device)stra") {
135
+ field(NELM, "3")
136
+ field(FTVL, "STRING")
137
+ field(INP, {const:["five", "six", "seven"]})
138
+ field(PINI, "YES")
139
+ }
140
+
141
+ record(waveform, "$(device)longstr") {
142
+ field(NELM, "80")
143
+ field(FTVL, "CHAR")
144
+ field(INP, {const:"a string that is just longer than forty characters"})
145
+ field(PINI, "YES")
146
+ }
147
+
148
+ record(lsi, "$(device)longstr2") {
149
+ field(SIZV, "80")
150
+ field(INP, {const:"a string that is just longer than forty characters"})
151
+ field(PINI, "YES")
152
+ }