epics-bridge 1.0.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.
- epics_bridge-1.0.0/PKG-INFO +193 -0
- epics_bridge-1.0.0/README.md +183 -0
- epics_bridge-1.0.0/epics_bridge/__init__.py +5 -0
- epics_bridge-1.0.0/epics_bridge/base_pv_interface.py +83 -0
- epics_bridge-1.0.0/epics_bridge/daemon.py +323 -0
- epics_bridge-1.0.0/epics_bridge/daemon_status.py +13 -0
- epics_bridge-1.0.0/epics_bridge/io.py +130 -0
- epics_bridge-1.0.0/epics_bridge/utils.py +39 -0
- epics_bridge-1.0.0/epics_bridge.egg-info/PKG-INFO +193 -0
- epics_bridge-1.0.0/epics_bridge.egg-info/SOURCES.txt +14 -0
- epics_bridge-1.0.0/epics_bridge.egg-info/dependency_links.txt +1 -0
- epics_bridge-1.0.0/epics_bridge.egg-info/requires.txt +2 -0
- epics_bridge-1.0.0/epics_bridge.egg-info/top_level.txt +1 -0
- epics_bridge-1.0.0/pyproject.toml +23 -0
- epics_bridge-1.0.0/setup.cfg +4 -0
- epics_bridge-1.0.0/tests/test_echo_daemon.py +315 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: epics-bridge
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A generic bridge between EPICS IOCs and Python logic.
|
|
5
|
+
Author-email: Hugo Valim <hugo.valim@ess.eu>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: p4p
|
|
10
|
+
|
|
11
|
+
# EPICS Bridge
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
**EPICS Bridge** is a high-availability Python framework designed for implementing a robust EPICS-Python interface. It provides a structured environment for bridging external control logic with the EPICS control system, emphasizing synchronous execution, fault tolerance, and strict process monitoring.
|
|
18
|
+
|
|
19
|
+
This library addresses the common reliability challenges like preventing silent stalls ("zombie processes") and handling network IO failures deterministically.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## System Architecture
|
|
23
|
+
|
|
24
|
+
The core of `epics-bridge` relies on a **Twin-Thread Architecture** that decouples the control logic from the monitoring signal.
|
|
25
|
+
|
|
26
|
+
### 1. Synchronous Control Loop (Main Thread)
|
|
27
|
+
The primary thread executes the user-defined logic in a strict, synchronous cycle:
|
|
28
|
+
1. **Trigger:** Waits for an input event or timer.
|
|
29
|
+
2. **Run Task:** Executes user-defined task
|
|
30
|
+
3. **Acknowledge:** Updates the task status and completes the handshake.
|
|
31
|
+
|
|
32
|
+
### 2. Isolated Heartbeat Monitor (Daemon Thread)
|
|
33
|
+
A separate, isolated thread acts as an internal watchdog. It monitors the activity timestamp of the Main Thread.
|
|
34
|
+
* **Operational:** Pulses the `Heartbeat` PV as long as the Main Thread is active.
|
|
35
|
+
* **Stalled (Zombie Protection):** If the Main Thread hangs (e.g., infinite loop, deadlocked IO) for longer than the defined tolerance, the Heartbeat thread ceases pulsing immediately. This alerts external watchdogs (e.g., the IOC or alarm handler) that the process is unresponsive.
|
|
36
|
+
|
|
37
|
+
### 3. Automatic Recovery ("Suicide Pact")
|
|
38
|
+
To support containerized environments (Docker, Kubernetes) or systemd supervisors, the daemon implements a fail-fast mechanism. If network connectivity is lost or IO errors persist beyond a configurable threshold (`max_stuck_cycles`), the process voluntarily terminates (`exit(0)`). This allows the external supervisor to perform a clean restart of the service.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### 4. Logger
|
|
42
|
+
Output important messages in the daemon shell to a configured log file.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Install the package
|
|
50
|
+
pip install .
|
|
51
|
+
|
|
52
|
+
# Install test dependencies
|
|
53
|
+
pip install -r requirements-test.txt
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Project Structure
|
|
60
|
+
|
|
61
|
+
- **epics_bridge.daemon**
|
|
62
|
+
Main control loop, heartbeat logic, and failure handling
|
|
63
|
+
|
|
64
|
+
- **epics_bridge.io**
|
|
65
|
+
Synchronous P4P client wrapper with strict error handling
|
|
66
|
+
|
|
67
|
+
- **epics_bridge.base_pv_interface**
|
|
68
|
+
PV template definitions and prefix validation
|
|
69
|
+
|
|
70
|
+
- **epics_bridge.utils**
|
|
71
|
+
Utilities for converting P4P data into native Python types
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
## Quick Start
|
|
76
|
+
### 1. EPICS Interface
|
|
77
|
+
There should be a standard epics db to handle the basic functionalities of the daemon and any amount of specialized dbs to fulfill the intended functionality.
|
|
78
|
+
|
|
79
|
+
The standard db should always be loaded by the IOC that interfaces with the daemon.
|
|
80
|
+
These are its contents:
|
|
81
|
+
|
|
82
|
+
```epics
|
|
83
|
+
record(bo, "$(P)Trigger") {
|
|
84
|
+
field(DESC, "Start Task")
|
|
85
|
+
field(ZNAM, "Idle")
|
|
86
|
+
field(ONAM, "Run")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
record(bi, "$(P)Busy") {
|
|
90
|
+
field(DESC, "Task Running Status")
|
|
91
|
+
field(ZNAM, "Idle")
|
|
92
|
+
field(ONAM, "Busy")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
record(bi, "$(P)Heartbeat") {
|
|
96
|
+
field(DESC, "Daemon Heartbeat")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
record(mbbi, "$(P)TaskStatus") {
|
|
100
|
+
field(DESC, "Last Cycle Result")
|
|
101
|
+
field(DTYP, "Raw Soft Channel")
|
|
102
|
+
|
|
103
|
+
# State 0: Success (Green)
|
|
104
|
+
field(ZRVL, "0")
|
|
105
|
+
field(ZRST, "Success")
|
|
106
|
+
field(ZRSV, "NO_ALARM")
|
|
107
|
+
|
|
108
|
+
# State 1: Logic Failure (Yellow - e.g. Interlock)
|
|
109
|
+
field(ONVL, "1")
|
|
110
|
+
field(ONST, "Task Fail")
|
|
111
|
+
field(ONSV, "MINOR")
|
|
112
|
+
|
|
113
|
+
# State 2: EPICS IO Failure (Yellow - e.g. PV Read/Write Error)
|
|
114
|
+
field(TWVL, "2")
|
|
115
|
+
field(TWST, "IO Failure")
|
|
116
|
+
field(TWSV, "MINOR")
|
|
117
|
+
|
|
118
|
+
# State 3: Exception (Red - Software/Hardware Crash)
|
|
119
|
+
field(THVL, "3")
|
|
120
|
+
field(THST, "Code Crash")
|
|
121
|
+
field(THSV, "MAJOR")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
record(ai, "$(P)TaskDuration") {
|
|
125
|
+
field(DESC, "Task duration")
|
|
126
|
+
field(PREC, "2")
|
|
127
|
+
field(EGU, "s")
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
### 2. Define a Python PV Interface
|
|
133
|
+
|
|
134
|
+
Use a dataclass to define EPICS PV templates.
|
|
135
|
+
Standard PVs (trigger, busy, heartbeat, task_status) are provided automatically.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from dataclasses import dataclass
|
|
139
|
+
from epics_bridge.base_pv_interface import BasePVInterface
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class MotorInterface(BasePVInterface):
|
|
143
|
+
position_rbv: str = "{main}Pos:RBV"
|
|
144
|
+
velocity_sp: str = "{main}Vel:SP"
|
|
145
|
+
temperature: str = "{sys}Temp:Mon"
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 3. Implement Control Logic
|
|
150
|
+
|
|
151
|
+
Subclass BridgeDaemon and implement the synchronous execute() method.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from epics_bridge.daemon import BridgeDaemon, TaskStatus
|
|
155
|
+
|
|
156
|
+
class MotorControlDaemon(BridgeDaemon):
|
|
157
|
+
def run_task(self, inputs=None) -> TaskStatus:
|
|
158
|
+
velocity = self.io.pvget(self.interface.velocity_sp)
|
|
159
|
+
|
|
160
|
+
if velocity is None:
|
|
161
|
+
return TaskStatus.ERROR
|
|
162
|
+
|
|
163
|
+
new_position = velocity * 0.5
|
|
164
|
+
|
|
165
|
+
self.io.pvput({
|
|
166
|
+
self.interface.position_rbv: new_position
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
return TaskStatus.DONE
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 4. Run the Daemon
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
|
|
176
|
+
def main():
|
|
177
|
+
|
|
178
|
+
prefixes = {
|
|
179
|
+
"main": "IOC:MOTOR:01:",
|
|
180
|
+
"sys": "IOC:SYS:"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface = MotorInterface(prefixes=prefixes)
|
|
184
|
+
|
|
185
|
+
daemon = MotorControlDaemon(
|
|
186
|
+
interface=interface,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
daemon.start()
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
main()
|
|
193
|
+
```
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# EPICS Bridge
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**EPICS Bridge** is a high-availability Python framework designed for implementing a robust EPICS-Python interface. It provides a structured environment for bridging external control logic with the EPICS control system, emphasizing synchronous execution, fault tolerance, and strict process monitoring.
|
|
8
|
+
|
|
9
|
+
This library addresses the common reliability challenges like preventing silent stalls ("zombie processes") and handling network IO failures deterministically.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## System Architecture
|
|
13
|
+
|
|
14
|
+
The core of `epics-bridge` relies on a **Twin-Thread Architecture** that decouples the control logic from the monitoring signal.
|
|
15
|
+
|
|
16
|
+
### 1. Synchronous Control Loop (Main Thread)
|
|
17
|
+
The primary thread executes the user-defined logic in a strict, synchronous cycle:
|
|
18
|
+
1. **Trigger:** Waits for an input event or timer.
|
|
19
|
+
2. **Run Task:** Executes user-defined task
|
|
20
|
+
3. **Acknowledge:** Updates the task status and completes the handshake.
|
|
21
|
+
|
|
22
|
+
### 2. Isolated Heartbeat Monitor (Daemon Thread)
|
|
23
|
+
A separate, isolated thread acts as an internal watchdog. It monitors the activity timestamp of the Main Thread.
|
|
24
|
+
* **Operational:** Pulses the `Heartbeat` PV as long as the Main Thread is active.
|
|
25
|
+
* **Stalled (Zombie Protection):** If the Main Thread hangs (e.g., infinite loop, deadlocked IO) for longer than the defined tolerance, the Heartbeat thread ceases pulsing immediately. This alerts external watchdogs (e.g., the IOC or alarm handler) that the process is unresponsive.
|
|
26
|
+
|
|
27
|
+
### 3. Automatic Recovery ("Suicide Pact")
|
|
28
|
+
To support containerized environments (Docker, Kubernetes) or systemd supervisors, the daemon implements a fail-fast mechanism. If network connectivity is lost or IO errors persist beyond a configurable threshold (`max_stuck_cycles`), the process voluntarily terminates (`exit(0)`). This allows the external supervisor to perform a clean restart of the service.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### 4. Logger
|
|
32
|
+
Output important messages in the daemon shell to a configured log file.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Install the package
|
|
40
|
+
pip install .
|
|
41
|
+
|
|
42
|
+
# Install test dependencies
|
|
43
|
+
pip install -r requirements-test.txt
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Project Structure
|
|
50
|
+
|
|
51
|
+
- **epics_bridge.daemon**
|
|
52
|
+
Main control loop, heartbeat logic, and failure handling
|
|
53
|
+
|
|
54
|
+
- **epics_bridge.io**
|
|
55
|
+
Synchronous P4P client wrapper with strict error handling
|
|
56
|
+
|
|
57
|
+
- **epics_bridge.base_pv_interface**
|
|
58
|
+
PV template definitions and prefix validation
|
|
59
|
+
|
|
60
|
+
- **epics_bridge.utils**
|
|
61
|
+
Utilities for converting P4P data into native Python types
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
## Quick Start
|
|
66
|
+
### 1. EPICS Interface
|
|
67
|
+
There should be a standard epics db to handle the basic functionalities of the daemon and any amount of specialized dbs to fulfill the intended functionality.
|
|
68
|
+
|
|
69
|
+
The standard db should always be loaded by the IOC that interfaces with the daemon.
|
|
70
|
+
These are its contents:
|
|
71
|
+
|
|
72
|
+
```epics
|
|
73
|
+
record(bo, "$(P)Trigger") {
|
|
74
|
+
field(DESC, "Start Task")
|
|
75
|
+
field(ZNAM, "Idle")
|
|
76
|
+
field(ONAM, "Run")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
record(bi, "$(P)Busy") {
|
|
80
|
+
field(DESC, "Task Running Status")
|
|
81
|
+
field(ZNAM, "Idle")
|
|
82
|
+
field(ONAM, "Busy")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
record(bi, "$(P)Heartbeat") {
|
|
86
|
+
field(DESC, "Daemon Heartbeat")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
record(mbbi, "$(P)TaskStatus") {
|
|
90
|
+
field(DESC, "Last Cycle Result")
|
|
91
|
+
field(DTYP, "Raw Soft Channel")
|
|
92
|
+
|
|
93
|
+
# State 0: Success (Green)
|
|
94
|
+
field(ZRVL, "0")
|
|
95
|
+
field(ZRST, "Success")
|
|
96
|
+
field(ZRSV, "NO_ALARM")
|
|
97
|
+
|
|
98
|
+
# State 1: Logic Failure (Yellow - e.g. Interlock)
|
|
99
|
+
field(ONVL, "1")
|
|
100
|
+
field(ONST, "Task Fail")
|
|
101
|
+
field(ONSV, "MINOR")
|
|
102
|
+
|
|
103
|
+
# State 2: EPICS IO Failure (Yellow - e.g. PV Read/Write Error)
|
|
104
|
+
field(TWVL, "2")
|
|
105
|
+
field(TWST, "IO Failure")
|
|
106
|
+
field(TWSV, "MINOR")
|
|
107
|
+
|
|
108
|
+
# State 3: Exception (Red - Software/Hardware Crash)
|
|
109
|
+
field(THVL, "3")
|
|
110
|
+
field(THST, "Code Crash")
|
|
111
|
+
field(THSV, "MAJOR")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
record(ai, "$(P)TaskDuration") {
|
|
115
|
+
field(DESC, "Task duration")
|
|
116
|
+
field(PREC, "2")
|
|
117
|
+
field(EGU, "s")
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
### 2. Define a Python PV Interface
|
|
123
|
+
|
|
124
|
+
Use a dataclass to define EPICS PV templates.
|
|
125
|
+
Standard PVs (trigger, busy, heartbeat, task_status) are provided automatically.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from dataclasses import dataclass
|
|
129
|
+
from epics_bridge.base_pv_interface import BasePVInterface
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class MotorInterface(BasePVInterface):
|
|
133
|
+
position_rbv: str = "{main}Pos:RBV"
|
|
134
|
+
velocity_sp: str = "{main}Vel:SP"
|
|
135
|
+
temperature: str = "{sys}Temp:Mon"
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 3. Implement Control Logic
|
|
140
|
+
|
|
141
|
+
Subclass BridgeDaemon and implement the synchronous execute() method.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from epics_bridge.daemon import BridgeDaemon, TaskStatus
|
|
145
|
+
|
|
146
|
+
class MotorControlDaemon(BridgeDaemon):
|
|
147
|
+
def run_task(self, inputs=None) -> TaskStatus:
|
|
148
|
+
velocity = self.io.pvget(self.interface.velocity_sp)
|
|
149
|
+
|
|
150
|
+
if velocity is None:
|
|
151
|
+
return TaskStatus.ERROR
|
|
152
|
+
|
|
153
|
+
new_position = velocity * 0.5
|
|
154
|
+
|
|
155
|
+
self.io.pvput({
|
|
156
|
+
self.interface.position_rbv: new_position
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return TaskStatus.DONE
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 4. Run the Daemon
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
|
|
166
|
+
def main():
|
|
167
|
+
|
|
168
|
+
prefixes = {
|
|
169
|
+
"main": "IOC:MOTOR:01:",
|
|
170
|
+
"sys": "IOC:SYS:"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface = MotorInterface(prefixes=prefixes)
|
|
174
|
+
|
|
175
|
+
daemon = MotorControlDaemon(
|
|
176
|
+
interface=interface,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
daemon.start()
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main()
|
|
183
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from dataclasses import dataclass, field, fields
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class BasePVInterface:
|
|
7
|
+
"""
|
|
8
|
+
A base configuration class for EPICS PV interfaces.
|
|
9
|
+
|
|
10
|
+
This class handles the dynamic construction of PV names based on provided
|
|
11
|
+
prefixes. It supports inheritance, allowing users to define sets of PVs
|
|
12
|
+
(templates) and instantiate them with specific device prefixes.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
prefixes (Dict[str, str]):
|
|
16
|
+
A dictionary of prefixes used to format the PV templates.
|
|
17
|
+
Example: {'sys': 'VAC:01:', 'main': 'PUMP:A:'}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# 1. Configuration: Holds the prefixes used for formatting
|
|
21
|
+
prefixes: Dict[str, str] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
# 2. Default PV Templates (Note: These use the '{main}' key)
|
|
24
|
+
trigger: str = "{main}Trigger"
|
|
25
|
+
heartbeat: str = "{main}Heartbeat"
|
|
26
|
+
busy: str = "{main}Busy"
|
|
27
|
+
task_status: str = "{main}TaskStatus"
|
|
28
|
+
task_duration: str = "{main}TaskDuration"
|
|
29
|
+
|
|
30
|
+
def __post_init__(self):
|
|
31
|
+
"""
|
|
32
|
+
Post-initialization hook.
|
|
33
|
+
Iterates through all string fields in the instance. If a field contains
|
|
34
|
+
Python format placeholders (e.g., "{sys}Name"), it formats the string
|
|
35
|
+
using the provided `prefixes` dictionary.
|
|
36
|
+
"""
|
|
37
|
+
missing_keys = []
|
|
38
|
+
|
|
39
|
+
for f in fields(self):
|
|
40
|
+
# Skip internal configuration fields
|
|
41
|
+
if f.name == "prefixes":
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# Get the raw value (the template, e.g., "{sys}Trigger")
|
|
45
|
+
raw_template = getattr(self, f.name)
|
|
46
|
+
|
|
47
|
+
# Only process strings that look like templates
|
|
48
|
+
if isinstance(raw_template, str) and "{" in raw_template:
|
|
49
|
+
try:
|
|
50
|
+
# FIX: Changed 'self.context' to 'self.prefixes'
|
|
51
|
+
formatted_pv = raw_template.format(**self.prefixes)
|
|
52
|
+
setattr(self, f.name, formatted_pv)
|
|
53
|
+
except KeyError as e:
|
|
54
|
+
# Collect errors to show them all at once
|
|
55
|
+
missing_keys.append(f"Field '{f.name}' requires prefix key {e}")
|
|
56
|
+
|
|
57
|
+
if missing_keys:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"Configuration Error: Missing prefixes for PV templates.\n"
|
|
60
|
+
+ "\n".join(missing_keys)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def as_dict(self) -> Dict[str, str]:
|
|
65
|
+
"""Returns a dictionary of PVs, excluding the internal 'prefixes'."""
|
|
66
|
+
# vars(self) is faster than asdict(self)
|
|
67
|
+
# We use .copy() to ensure we don't modify the actual object
|
|
68
|
+
data = vars(self).copy()
|
|
69
|
+
|
|
70
|
+
# Remove the configuration field so you only get PVs
|
|
71
|
+
if "prefixes" in data:
|
|
72
|
+
del data["prefixes"]
|
|
73
|
+
|
|
74
|
+
return data
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def pv_to_attr(self) -> Dict[str, str]:
|
|
78
|
+
"""
|
|
79
|
+
Returns a reverse mapping: {Formatted_PV_Name: Attribute_Name}.
|
|
80
|
+
Example: {'VAC:01:Trigger': 'trigger'}
|
|
81
|
+
"""
|
|
82
|
+
# Invert the dictionary: value becomes key, key becomes value
|
|
83
|
+
return {v: k for k, v in self.as_dict.items()}
|