pyaml-cs-oa 0.1.0__tar.gz
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.
- pyaml_cs_oa-0.1.0/.github/workflows/deploy-pypi.yaml +35 -0
- pyaml_cs_oa-0.1.0/.gitignore +2 -0
- pyaml_cs_oa-0.1.0/.pre-commit-config.yaml +21 -0
- pyaml_cs_oa-0.1.0/PKG-INFO +93 -0
- pyaml_cs_oa-0.1.0/README.md +62 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/__init__.py +79 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/container.py +170 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/controlsystem.py +154 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/epics.py +53 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/epicsR.py +13 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/epicsRW.py +13 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/epicsW.py +13 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/float_signal.py +61 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/scalar_aggregator.py +77 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/signal.py +88 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/tango.py +39 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/tangoR.py +13 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/tangoRW.py +13 -0
- pyaml_cs_oa-0.1.0/pyaml_cs_oa/types.py +35 -0
- pyaml_cs_oa-0.1.0/pyproject.toml +84 -0
- pyaml_cs_oa-0.1.0/tests/EBSTune-ophyd.yaml +1893 -0
- pyaml_cs_oa-0.1.0/tests/QD2_strength.csv +101 -0
- pyaml_cs_oa-0.1.0/tests/QF1_strength.csv +101 -0
- pyaml_cs_oa-0.1.0/tests/bessy2.mat +0 -0
- pyaml_cs_oa-0.1.0/tests/bessy2tune-KL.yaml +515 -0
- pyaml_cs_oa-0.1.0/tests/bessy2tune.yaml +519 -0
- pyaml_cs_oa-0.1.0/tests/convert_tokl.py +71 -0
- pyaml_cs_oa-0.1.0/tests/ebs.mat +0 -0
- pyaml_cs_oa-0.1.0/tests/test-tune-bessy.py +31 -0
- pyaml_cs_oa-0.1.0/tests/test-tune.py +25 -0
- pyaml_cs_oa-0.1.0/tests/tunemat-bessy.json +1 -0
- pyaml_cs_oa-0.1.0/tests/tunemat.json +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish Python Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- '[0-9]+.[0-9]+.[0-9]+'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
deploy:
|
|
11
|
+
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
#environment: release
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: '3.11'
|
|
24
|
+
cache: pip
|
|
25
|
+
cache-dependency-path: '**/pyproject.toml'
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install hatch
|
|
30
|
+
- name: Build package
|
|
31
|
+
run: hatch build
|
|
32
|
+
#- name: Test package
|
|
33
|
+
# run: hatch run test
|
|
34
|
+
- name: Publish
|
|
35
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v6.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
|
|
8
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
9
|
+
rev: v0.14.5
|
|
10
|
+
hooks:
|
|
11
|
+
- id: ruff
|
|
12
|
+
args: [--fix]
|
|
13
|
+
- id: ruff-format
|
|
14
|
+
|
|
15
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
16
|
+
rev: v1.18.2
|
|
17
|
+
hooks:
|
|
18
|
+
- id: mypy
|
|
19
|
+
additional_dependencies: [
|
|
20
|
+
"pydantic>=2.0",
|
|
21
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyaml-cs-oa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PyAML control system plugin for ophyd-async
|
|
5
|
+
Maintainer-email: Yoshiteru Hidaka <yhidaka@bnl.gov>
|
|
6
|
+
Keywords: Accelerator,Commissioning,Digital Twin,EPICS,Operation,Synchrotron,Tango,Tuning
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: Natural Language :: English
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: ophyd-async
|
|
17
|
+
Requires-Dist: pyaml
|
|
18
|
+
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
21
|
+
Requires-Dist: ophyd-async[ca,pva]; extra == 'dev'
|
|
22
|
+
Requires-Dist: ophyd-async[tango]; extra == 'dev'
|
|
23
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
26
|
+
Provides-Extra: epics
|
|
27
|
+
Requires-Dist: ophyd-async[ca,pva]; extra == 'epics'
|
|
28
|
+
Provides-Extra: tango
|
|
29
|
+
Requires-Dist: ophyd-async[tango]; extra == 'tango'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# `pyaml-cs-oa`
|
|
33
|
+
|
|
34
|
+
**PyAML control system plugin for ophyd-async**
|
|
35
|
+
|
|
36
|
+
`pyaml-cs-oa` is a plugin for `PyAML` based on `ophyd-async`, which
|
|
37
|
+
currently supports EPICS and Tango control systems.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🔧 Installation
|
|
42
|
+
|
|
43
|
+
### **Requirements**
|
|
44
|
+
|
|
45
|
+
- Python **3.11+**
|
|
46
|
+
|
|
47
|
+
- Depending on your runtime environment, you may want to install support for EPICS or Tango.
|
|
48
|
+
|
|
49
|
+
### **EPICS CA/PVA Support**
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
pip install pyaml-cs-oa[epics]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This installs:
|
|
56
|
+
|
|
57
|
+
- `ophyd-async[ca,pva]`
|
|
58
|
+
|
|
59
|
+
### **Tango Support**
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
pip install pyaml-cs-oa[tango]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This installs:
|
|
66
|
+
|
|
67
|
+
- `ophyd-async[tango]`
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 🧪 Developer Installation
|
|
72
|
+
|
|
73
|
+
If you are contributing, debugging, or running the test suite (no test
|
|
74
|
+
currently provided):
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
pip install pyaml-cs-oa[dev]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This installs:
|
|
81
|
+
|
|
82
|
+
- `ophyd-async[ca,pva]`
|
|
83
|
+
- `ophyd-async[tango]`
|
|
84
|
+
- `pre-commit`
|
|
85
|
+
- `ruff`
|
|
86
|
+
- `mypy`
|
|
87
|
+
- `pytest`
|
|
88
|
+
|
|
89
|
+
### Setup pre-commit hooks
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
pre-commit install
|
|
93
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# `pyaml-cs-oa`
|
|
2
|
+
|
|
3
|
+
**PyAML control system plugin for ophyd-async**
|
|
4
|
+
|
|
5
|
+
`pyaml-cs-oa` is a plugin for `PyAML` based on `ophyd-async`, which
|
|
6
|
+
currently supports EPICS and Tango control systems.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 🔧 Installation
|
|
11
|
+
|
|
12
|
+
### **Requirements**
|
|
13
|
+
|
|
14
|
+
- Python **3.11+**
|
|
15
|
+
|
|
16
|
+
- Depending on your runtime environment, you may want to install support for EPICS or Tango.
|
|
17
|
+
|
|
18
|
+
### **EPICS CA/PVA Support**
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
pip install pyaml-cs-oa[epics]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This installs:
|
|
25
|
+
|
|
26
|
+
- `ophyd-async[ca,pva]`
|
|
27
|
+
|
|
28
|
+
### **Tango Support**
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
pip install pyaml-cs-oa[tango]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This installs:
|
|
35
|
+
|
|
36
|
+
- `ophyd-async[tango]`
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 🧪 Developer Installation
|
|
41
|
+
|
|
42
|
+
If you are contributing, debugging, or running the test suite (no test
|
|
43
|
+
currently provided):
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
pip install pyaml-cs-oa[dev]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This installs:
|
|
50
|
+
|
|
51
|
+
- `ophyd-async[ca,pva]`
|
|
52
|
+
- `ophyd-async[tango]`
|
|
53
|
+
- `pre-commit`
|
|
54
|
+
- `ruff`
|
|
55
|
+
- `mypy`
|
|
56
|
+
- `pytest`
|
|
57
|
+
|
|
58
|
+
### Setup pre-commit hooks
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
pre-commit install
|
|
62
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
from typing import Awaitable, Any
|
|
4
|
+
import atexit
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
# One persistent event loop
|
|
9
|
+
_loop = None
|
|
10
|
+
_nest_asyncio_applied = False
|
|
11
|
+
|
|
12
|
+
def loop() -> asyncio.AbstractEventLoop:
|
|
13
|
+
|
|
14
|
+
global _loop, _nest_asyncio_applied
|
|
15
|
+
|
|
16
|
+
# Try to get the currently running loop (e.g., in Jupyter)
|
|
17
|
+
try:
|
|
18
|
+
running_loop = asyncio.get_running_loop()
|
|
19
|
+
# We found a running loop (Jupyter case)
|
|
20
|
+
if not _nest_asyncio_applied:
|
|
21
|
+
try:
|
|
22
|
+
import nest_asyncio
|
|
23
|
+
nest_asyncio.apply(running_loop)
|
|
24
|
+
_nest_asyncio_applied = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
pass
|
|
27
|
+
return running_loop
|
|
28
|
+
except RuntimeError:
|
|
29
|
+
# No running loop, create our own
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
if _loop is None or _loop.is_closed():
|
|
33
|
+
_loop = asyncio.new_event_loop()
|
|
34
|
+
asyncio.set_event_loop(_loop)
|
|
35
|
+
|
|
36
|
+
# Apply nest_asyncio to our new loop
|
|
37
|
+
if not _nest_asyncio_applied:
|
|
38
|
+
try:
|
|
39
|
+
import nest_asyncio
|
|
40
|
+
nest_asyncio.apply(_loop)
|
|
41
|
+
_nest_asyncio_applied = True
|
|
42
|
+
except ImportError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
return _loop
|
|
46
|
+
loop() # Make sure to initialize `_loop`
|
|
47
|
+
|
|
48
|
+
def _reap_done_tasks(evloop: asyncio.AbstractEventLoop) -> None:
|
|
49
|
+
"""Reap exceptions from tasks that are already DONE on this loop.
|
|
50
|
+
Does not cancel or otherwise touch pending tasks.
|
|
51
|
+
"""
|
|
52
|
+
if evloop is None or evloop.is_closed():
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Snapshot; tasks may change during iteration
|
|
57
|
+
for t in tuple(asyncio.all_tasks(evloop)):
|
|
58
|
+
if not t.done():
|
|
59
|
+
continue
|
|
60
|
+
# If the task is cancelled, .result() raises CancelledError — suppress that.
|
|
61
|
+
# If the task failed, .result() raises its exception — suppress to just mark it retrieved.
|
|
62
|
+
with contextlib.suppress(asyncio.CancelledError, Exception):
|
|
63
|
+
t.result()
|
|
64
|
+
except (RuntimeError, AttributeError):
|
|
65
|
+
# During shutdown, asyncio may be partially cleaned up
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def arun(coro: Awaitable[Any]) -> Any:
|
|
70
|
+
|
|
71
|
+
evloop = loop()
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
return evloop.run_until_complete(coro)
|
|
75
|
+
finally:
|
|
76
|
+
# Clean up completed/cancelled tasks so residual CancelledError
|
|
77
|
+
# doesn't leak to next run
|
|
78
|
+
_reap_done_tasks(evloop)
|
|
79
|
+
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
from .signal import OASignal
|
|
6
|
+
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
SignalDatatypeT,
|
|
9
|
+
SignalR,
|
|
10
|
+
SignalW,
|
|
11
|
+
set_and_wait_for_other_value,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
from . import arun
|
|
17
|
+
|
|
18
|
+
def _looks_disconnected(exc: BaseException) -> bool:
|
|
19
|
+
# Keep it generic: ophyd-async wraps cancellations in TimeoutError;
|
|
20
|
+
# tango/epics transports often raise CancelledError or "NotConnected" types.
|
|
21
|
+
return isinstance(exc, (asyncio.CancelledError, TimeoutError))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def _recover_once(
|
|
25
|
+
run: Callable[[], Awaitable[T]],
|
|
26
|
+
reconnect: Callable[[], Awaitable[None]],
|
|
27
|
+
peer: OASignal,
|
|
28
|
+
) -> T:
|
|
29
|
+
try:
|
|
30
|
+
return await run()
|
|
31
|
+
except BaseException as exc:
|
|
32
|
+
if not _looks_disconnected(exc):
|
|
33
|
+
raise
|
|
34
|
+
# Attempt reconnect of the same Signal first
|
|
35
|
+
try:
|
|
36
|
+
await reconnect()
|
|
37
|
+
return await run()
|
|
38
|
+
except BaseException:
|
|
39
|
+
# If that fails and we have a way to rebuild, do so and try one more time
|
|
40
|
+
if peer is not None:
|
|
41
|
+
maybe_awaitable = peer.build()
|
|
42
|
+
if inspect.isawaitable(maybe_awaitable):
|
|
43
|
+
await maybe_awaitable
|
|
44
|
+
await reconnect()
|
|
45
|
+
return await run()
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class OAReadback():
|
|
50
|
+
"""A readback object."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, r_signal: SignalR[SignalDatatypeT]):
|
|
53
|
+
self._r_sig = r_signal
|
|
54
|
+
|
|
55
|
+
async def _run_get(self) -> SignalDatatypeT:
|
|
56
|
+
await self._r_sig.connect()
|
|
57
|
+
backend = self._r_sig._connector.backend
|
|
58
|
+
return await backend.get_value()
|
|
59
|
+
|
|
60
|
+
async def async_get(self) -> SignalDatatypeT:
|
|
61
|
+
return await _recover_once(
|
|
62
|
+
self._run_get,
|
|
63
|
+
self._r_sig.connect,
|
|
64
|
+
getattr(self._r_sig, "__peer__", None),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def _run_read(self) -> SignalDatatypeT:
|
|
68
|
+
await self._r_sig.connect()
|
|
69
|
+
backend = self._r_sig._connector.backend
|
|
70
|
+
return await backend.get_reading()
|
|
71
|
+
|
|
72
|
+
async def async_read(self) -> SignalDatatypeT:
|
|
73
|
+
return await _recover_once(
|
|
74
|
+
self._run_read,
|
|
75
|
+
self._r_sig.connect,
|
|
76
|
+
getattr(self._r_sig, "__peer__", None),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def get(self) -> SignalDatatypeT:
|
|
80
|
+
"""Synchronous wrapper around `async_get()`."""
|
|
81
|
+
return arun(self.async_get())
|
|
82
|
+
|
|
83
|
+
class OASetpoint():
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
w_signal: SignalW[SignalDatatypeT],
|
|
87
|
+
r_signal: SignalR[SignalDatatypeT] | None = None,
|
|
88
|
+
):
|
|
89
|
+
self._w_sig = w_signal
|
|
90
|
+
self._r_sig = r_signal # used only for `set_and_wait()`
|
|
91
|
+
self._has_r_sig = (r_signal is not None)
|
|
92
|
+
|
|
93
|
+
async def _run_get(self) -> SignalDatatypeT:
|
|
94
|
+
await self._w_sig.connect()
|
|
95
|
+
backend = self._w_sig._connector.backend
|
|
96
|
+
return await backend.get_setpoint()
|
|
97
|
+
|
|
98
|
+
async def async_get(self) -> SignalDatatypeT:
|
|
99
|
+
return await _recover_once(
|
|
100
|
+
self._run_get,
|
|
101
|
+
self._w_sig.connect,
|
|
102
|
+
getattr(self._w_sig, "__peer__", None),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def _run_read(self) -> SignalDatatypeT:
|
|
106
|
+
await self._w_sig.connect()
|
|
107
|
+
backend = self._w_sig._connector.backend
|
|
108
|
+
return await backend.get_reading()
|
|
109
|
+
|
|
110
|
+
async def async_read(self) -> SignalDatatypeT:
|
|
111
|
+
return await _recover_once(
|
|
112
|
+
self._run_read,
|
|
113
|
+
self._w_sig.connect,
|
|
114
|
+
getattr(self._w_sig, "__peer__", None),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def _run_set(self, value):
|
|
118
|
+
await self._w_sig.connect()
|
|
119
|
+
status = self._w_sig.set(value)
|
|
120
|
+
return status
|
|
121
|
+
|
|
122
|
+
async def async_set(self, value):
|
|
123
|
+
return await _recover_once(
|
|
124
|
+
lambda: self._run_set(value),
|
|
125
|
+
self._w_sig.connect,
|
|
126
|
+
getattr(self._w_sig, "__peer__", None),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def _reconnect_both(self) -> None:
|
|
130
|
+
await asyncio.gather(self._w_sig.connect(), self._r_sig.connect())
|
|
131
|
+
|
|
132
|
+
async def _rebuild_both(self) -> None:
|
|
133
|
+
w_rebuild = getattr(self._w_sig, "__peer__", None)
|
|
134
|
+
r_rebuild = getattr(self._r_sig, "__peer__", None)
|
|
135
|
+
if w_rebuild is not None:
|
|
136
|
+
w_rebuild()
|
|
137
|
+
if r_rebuild is not None:
|
|
138
|
+
r_rebuild()
|
|
139
|
+
|
|
140
|
+
async def _run_set_and_wait(self, value) -> None:
|
|
141
|
+
if not self._has_r_sig:
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
"Cannot use set_and_wait() without a matching readback signal."
|
|
144
|
+
)
|
|
145
|
+
await self._reconnect_both()
|
|
146
|
+
await set_and_wait_for_other_value(self._w_sig, value, self._r_sig, value)
|
|
147
|
+
|
|
148
|
+
async def async_set_and_wait(self, value) -> None:
|
|
149
|
+
return await _recover_once(
|
|
150
|
+
lambda: self._run_set_and_wait(value),
|
|
151
|
+
self._reconnect_both,
|
|
152
|
+
self._rebuild_both,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def _complete_set(self, value):
|
|
156
|
+
status = await self.async_set(value)
|
|
157
|
+
await status # Wait for completion before returning
|
|
158
|
+
return status
|
|
159
|
+
|
|
160
|
+
def set(self, value):
|
|
161
|
+
"""Synchronous wrapper around `async_set()`."""
|
|
162
|
+
return arun(self._complete_set(value))
|
|
163
|
+
|
|
164
|
+
def get(self) -> SignalDatatypeT:
|
|
165
|
+
"""Synchronous wrapper around `async_get()`."""
|
|
166
|
+
return arun(self.async_get())
|
|
167
|
+
|
|
168
|
+
def set_and_wait(self, value) -> None:
|
|
169
|
+
"""Synchronous wrapper around `async_set_and_wait()`."""
|
|
170
|
+
return arun(self.async_set_and_wait(value))
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
from pyaml.control.controlsystem import ControlSystem
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from pyaml.common.exception import PyAMLException
|
|
8
|
+
|
|
9
|
+
PYAMLCLASS : str = "OphydAsyncControlSystem"
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
from .types import (
|
|
14
|
+
EpicsConfigR,
|
|
15
|
+
EpicsConfigW,
|
|
16
|
+
EpicsConfigRW,
|
|
17
|
+
TangoConfigR,
|
|
18
|
+
TangoConfigRW,
|
|
19
|
+
)
|
|
20
|
+
from .signal import OASignal
|
|
21
|
+
from .epicsR import EpicsR
|
|
22
|
+
from .epicsW import EpicsW
|
|
23
|
+
from .epicsRW import EpicsRW
|
|
24
|
+
from .tangoR import TangoR
|
|
25
|
+
from .tangoRW import TangoRW
|
|
26
|
+
|
|
27
|
+
class ConfigModel(BaseModel):
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
Configuration model for an OA Control System.
|
|
31
|
+
|
|
32
|
+
Attributes
|
|
33
|
+
----------
|
|
34
|
+
name : str
|
|
35
|
+
Name of the control system.
|
|
36
|
+
prefix : str
|
|
37
|
+
Prefix added to the PV or attribute name. It can be a
|
|
38
|
+
for instance, TANGO_HOST, or a PV prefix.
|
|
39
|
+
debug_level : int
|
|
40
|
+
Debug verbosity level.
|
|
41
|
+
scalar_aggregator : str
|
|
42
|
+
Aggregator module for scalar values. If none specified, writings and
|
|
43
|
+
readings of sclar value are serialized.
|
|
44
|
+
vector_aggregator : str
|
|
45
|
+
Aggregator module for vecrors. If none specified, writings and readings
|
|
46
|
+
of vector are serialized,
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
prefix: str = ""
|
|
51
|
+
debug_level: str=None
|
|
52
|
+
scalar_aggregator: str | None = "pyaml_cs_oa.scalar_aggregator"
|
|
53
|
+
vector_aggregator: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OphydAsyncControlSystem(ControlSystem):
|
|
57
|
+
"""A generic control system using ophyd_async backend."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, cfg: ConfigModel):
|
|
60
|
+
super().__init__()
|
|
61
|
+
self._cfg = cfg
|
|
62
|
+
self._devices = {} # Dict containing all attached DeviceAccess
|
|
63
|
+
|
|
64
|
+
if self._cfg.debug_level:
|
|
65
|
+
log_level = getattr(logging, self._cfg.debug_level, logging.WARNING)
|
|
66
|
+
logger.parent.setLevel(log_level)
|
|
67
|
+
logger.setLevel(log_level)
|
|
68
|
+
|
|
69
|
+
logger.log(logging.WARNING, f"OA control system binding for PyAML initialized with name '{self._cfg.name}'"
|
|
70
|
+
f" and prefix='{self._cfg.prefix}'")
|
|
71
|
+
|
|
72
|
+
def attach(self, devs: list[OASignal]) -> list[OASignal]:
|
|
73
|
+
return self._attach(devs,False)
|
|
74
|
+
|
|
75
|
+
def attach_array(self, devs: list[OASignal]) -> list[OASignal]:
|
|
76
|
+
return self._attach(devs,True)
|
|
77
|
+
|
|
78
|
+
def _attach(self, devs: list[OASignal],is_array:bool) -> list[OASignal]:
|
|
79
|
+
# Concatenate the prefix
|
|
80
|
+
newDevs = []
|
|
81
|
+
for d in devs:
|
|
82
|
+
if d is not None:
|
|
83
|
+
|
|
84
|
+
sig_cfg = d._cfg
|
|
85
|
+
sig_cfg_cls = sig_cfg.__class__
|
|
86
|
+
|
|
87
|
+
if isinstance(d._cfg,EpicsConfigR):
|
|
88
|
+
key = self._cfg.prefix + d._cfg.read_pvname
|
|
89
|
+
sig_cls = EpicsR
|
|
90
|
+
config = dict(read_pvname=key,timeout_ms=d._cfg.timeout_ms)
|
|
91
|
+
elif isinstance(d._cfg,EpicsConfigW):
|
|
92
|
+
key = self._cfg.prefix + d._cfg.write_pvname
|
|
93
|
+
sig_cls = EpicsW
|
|
94
|
+
config = dict(write_pvname=key,timeout_ms=d._cfg.timeout_ms)
|
|
95
|
+
elif isinstance(d._cfg,EpicsConfigRW):
|
|
96
|
+
key = self._cfg.prefix + d._cfg.read_pvname + d._cfg.write_pvname
|
|
97
|
+
sig_cls = EpicsRW
|
|
98
|
+
config = dict(read_pvname=self._cfg.prefix + d._cfg.read_pvname, write_pvname=self._cfg.prefix + d._cfg.write_pvname,timeout_ms=d._cfg.timeout_ms)
|
|
99
|
+
elif isinstance(d._cfg,TangoConfigR):
|
|
100
|
+
key = self._cfg.prefix + d._cfg.attribute
|
|
101
|
+
sig_cls = TangoR
|
|
102
|
+
config = dict(attribute=key,timeout_ms=d._cfg.timeout_ms)
|
|
103
|
+
elif isinstance(d._cfg,TangoConfigRW):
|
|
104
|
+
key = self._cfg.prefix + d._cfg.attribute
|
|
105
|
+
sig_cls = TangoRW
|
|
106
|
+
config = dict(attribute=key,timeout_ms=d._cfg.timeout_ms)
|
|
107
|
+
else:
|
|
108
|
+
raise PyAMLException(f"OphydAsyncControlSystem: Unsupported type {type(sig_cfg)}")
|
|
109
|
+
|
|
110
|
+
if key not in self._devices:
|
|
111
|
+
nr = sig_cls(sig_cfg_cls(**config),is_array)
|
|
112
|
+
nr.build()
|
|
113
|
+
self._devices[key] = nr
|
|
114
|
+
|
|
115
|
+
newDevs.append(self._devices[key])
|
|
116
|
+
else:
|
|
117
|
+
newDevs.append(None)
|
|
118
|
+
return newDevs
|
|
119
|
+
|
|
120
|
+
def name(self) -> str:
|
|
121
|
+
"""
|
|
122
|
+
Return the name of the control system.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
str
|
|
127
|
+
Name of the control system.
|
|
128
|
+
"""
|
|
129
|
+
return self._cfg.name
|
|
130
|
+
|
|
131
|
+
def scalar_aggregator(self) -> str | None:
|
|
132
|
+
"""
|
|
133
|
+
Returns the module name used for handling aggregator of DeviceAccess
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
str
|
|
138
|
+
Aggregator module name
|
|
139
|
+
"""
|
|
140
|
+
return self._cfg.scalar_aggregator
|
|
141
|
+
|
|
142
|
+
def vector_aggregator(self) -> str | None:
|
|
143
|
+
"""
|
|
144
|
+
Returns the module name used for handling aggregator of DeviceVectorAccess
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
str
|
|
149
|
+
Aggregator module name
|
|
150
|
+
"""
|
|
151
|
+
return self._cfg.vector_aggregator
|
|
152
|
+
|
|
153
|
+
def __repr__(self):
|
|
154
|
+
return repr(self._cfg).replace("ConfigModel",self.__class__.__name__)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from ophyd_async.epics.signal import epics_signal_r, epics_signal_w, epics_signal_rw
|
|
2
|
+
from ophyd_async.core import Array1D
|
|
3
|
+
import numpy
|
|
4
|
+
|
|
5
|
+
from .container import OAReadback as Readback
|
|
6
|
+
from .container import OASetpoint as Setpoint
|
|
7
|
+
from .types import (
|
|
8
|
+
ControlSysConfig,
|
|
9
|
+
EpicsConfigR,
|
|
10
|
+
EpicsConfigRW,
|
|
11
|
+
EpicsConfigW,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_SP_RB(cfg: ControlSysConfig,is_array:bool) -> tuple[Setpoint | None, Readback | None]:
|
|
16
|
+
setpoint: Setpoint | None = None
|
|
17
|
+
readback: Readback | None = None
|
|
18
|
+
|
|
19
|
+
assert isinstance(cfg, (EpicsConfigRW, EpicsConfigR, EpicsConfigW))
|
|
20
|
+
|
|
21
|
+
if isinstance(cfg, EpicsConfigR):
|
|
22
|
+
r_sig = epics_signal_r(
|
|
23
|
+
datatype=float if not is_array else Array1D[numpy.float64],
|
|
24
|
+
read_pv=cfg.read_pvname,
|
|
25
|
+
name="",
|
|
26
|
+
timeout = cfg.timeout_ms / 1000.,
|
|
27
|
+
)
|
|
28
|
+
readback = Readback(r_sig)
|
|
29
|
+
setpoint = None
|
|
30
|
+
|
|
31
|
+
if isinstance(cfg, EpicsConfigW):
|
|
32
|
+
w_sig = epics_signal_w(
|
|
33
|
+
datatype=float if not is_array else Array1D[numpy.float64],
|
|
34
|
+
write_pv=cfg.write_pvname,
|
|
35
|
+
name="",
|
|
36
|
+
timeout = cfg.timeout_ms / 1000.,
|
|
37
|
+
)
|
|
38
|
+
readback = None
|
|
39
|
+
setpoint = Setpoint(w_sig)
|
|
40
|
+
|
|
41
|
+
if isinstance(cfg, EpicsConfigRW):
|
|
42
|
+
w_sig = epics_signal_rw(
|
|
43
|
+
datatype=float if not is_array else Array1D[numpy.float64],
|
|
44
|
+
read_pv=cfg.read_pvname,
|
|
45
|
+
write_pv=cfg.write_pvname,
|
|
46
|
+
name="",
|
|
47
|
+
timeout = cfg.timeout_ms / 1000.,
|
|
48
|
+
)
|
|
49
|
+
readback = Readback(w_sig)
|
|
50
|
+
setpoint = Setpoint(w_sig)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
return setpoint, readback
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .float_signal import FloatSignalContainer
|
|
2
|
+
from .types import EpicsConfigR
|
|
3
|
+
|
|
4
|
+
PYAMLCLASS : str = "EpicsR"
|
|
5
|
+
|
|
6
|
+
class ConfigModel(EpicsConfigR):
|
|
7
|
+
unit: str = ""
|
|
8
|
+
|
|
9
|
+
class EpicsR(FloatSignalContainer):
|
|
10
|
+
def __init__(self, cfg: ConfigModel, is_array=False):
|
|
11
|
+
super().__init__(cfg,is_array)
|
|
12
|
+
def get_cs(self) -> str:
|
|
13
|
+
return "epics"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .float_signal import FloatSignalContainer
|
|
2
|
+
from .types import EpicsConfigRW
|
|
3
|
+
|
|
4
|
+
PYAMLCLASS : str = "EpicsRW"
|
|
5
|
+
|
|
6
|
+
class ConfigModel(EpicsConfigRW):
|
|
7
|
+
unit: str = ""
|
|
8
|
+
|
|
9
|
+
class EpicsRW(FloatSignalContainer):
|
|
10
|
+
def __init__(self, cfg: ConfigModel, is_array=False):
|
|
11
|
+
super().__init__(cfg,is_array)
|
|
12
|
+
def get_cs(self) -> str:
|
|
13
|
+
return "epics"
|