epicsdev 3.1.4__tar.gz → 3.1.5__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.
- {epicsdev-3.1.4 → epicsdev-3.1.5}/PKG-INFO +3 -2
- {epicsdev-3.1.4 → epicsdev-3.1.5}/README.md +1 -1
- {epicsdev-3.1.4 → epicsdev-3.1.5}/epicsdev/epicsdev.py +11 -5
- epicsdev-3.1.5/new/README.md +133 -0
- epicsdev-3.1.5/new/epicsdev.py +631 -0
- epicsdev-3.1.5/new/multiadc.py +183 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5/new}/pyproject.toml +2 -1
- epicsdev-3.1.5/pyproject.toml +25 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/.github/copilot-instructions.md +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/LICENSE +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/config/epicsSimscope_pp.py +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/config/epicsdev.bob +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/config/epicsdev_pp.py +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/config/multiadc1_pp.py +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/config/multiadc_pp.py +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/docs/epicsdev_pvplot.jpg +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/docs/epicsdev_pypet.png +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/docs/modules.md +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/docs/phoebus_epicsdev.jpg +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/epicsdev/__init__.py +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/epicsdev/multiadc.py +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/epicsdev/putlog.py +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/fallback/epicsdev-300.py +0 -0
- {epicsdev-3.1.4 → epicsdev-3.1.5}/fallback/multiadc.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: epicsdev
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.5
|
|
4
4
|
Summary: Helper module for creating EPICS PVAccess servers using p4p
|
|
5
5
|
Project-URL: Homepage, https://github.com/ASukhanov/epicsdev
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/ASukhanov/epicsdev
|
|
@@ -11,6 +11,7 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.7
|
|
13
13
|
Requires-Dist: p4p>=4.2.2
|
|
14
|
+
Requires-Dist: psutil
|
|
14
15
|
Description-Content-Type: text/markdown
|
|
15
16
|
|
|
16
17
|
# epicsdev
|
|
@@ -102,7 +103,7 @@ python -m epicsdev.putlog /tmp/putlog.txt
|
|
|
102
103
|
Default PV prefix is `putlog0:`, so write text to:
|
|
103
104
|
|
|
104
105
|
```bash
|
|
105
|
-
|
|
106
|
+
pvput putlog0:dump "hello from client"
|
|
106
107
|
```
|
|
107
108
|
---
|
|
108
109
|
## AI-Assisted Device Support Development
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Helper functions for creating EPICS PVAccess server"""
|
|
2
2
|
# pylint: disable=invalid-name
|
|
3
|
-
__version__= 'v3.1.
|
|
3
|
+
__version__= 'v3.1.5 26-03-16'# Setters for enums were not working, recovered.
|
|
4
4
|
# SPV removed, PvDefs definitions simplified, new features added.
|
|
5
5
|
#TODO: add support for autosave, (feature 'A'), caputLog (feature 'H') and access rights
|
|
6
6
|
|
|
@@ -251,8 +251,8 @@ def create_PVs(pvDefs, pvcache=None):
|
|
|
251
251
|
if spv.setter:
|
|
252
252
|
spv.setter(vr, spv)
|
|
253
253
|
# value will be updated by the setter, so get it again
|
|
254
|
-
|
|
255
|
-
vr = spv._wrap(spv.current())['value']
|
|
254
|
+
vr = pvv(spv.name)
|
|
255
|
+
#vr = spv._wrap(spv.current())['value']
|
|
256
256
|
printv(f'putting {spv.name} = {vr}')
|
|
257
257
|
ct = time.time()
|
|
258
258
|
C_.lastPutTime = ct
|
|
@@ -263,7 +263,8 @@ def create_PVs(pvDefs, pvcache=None):
|
|
|
263
263
|
ip = op.peer().split(':')[3][:-1]# peer looks like: [::ffff:192.168.27.6]:46362
|
|
264
264
|
jmsg = {"date":dt[0], "time":dt[1],
|
|
265
265
|
"host":ip, "user":op.account(),
|
|
266
|
-
"pv":op.name(), "new":vr, "old":oldvr}
|
|
266
|
+
"pv":op.name(), "new":str(vr), "old":str(oldvr)}
|
|
267
|
+
printv(f'Logging put operation: {jmsg}')
|
|
267
268
|
s = json.dumps(jmsg)
|
|
268
269
|
try:
|
|
269
270
|
IFace.put(C_.putlogPV, "'"+s+"'", timeout=0.5)# quote the string to avoid interpreting it as JSON
|
|
@@ -423,7 +424,11 @@ def init_epicsdev(prefix:str, pvDefs:list, verbose=0, serverStateChanged=None,
|
|
|
423
424
|
pvs = create_pvDefs(pvDefs, pvcache)
|
|
424
425
|
# Set up autosave if requested. That will save PV values to a file, and restore them on the next startup.
|
|
425
426
|
if autosaveDir is not None:
|
|
426
|
-
|
|
427
|
+
try:
|
|
428
|
+
os.makedirs(autosaveDir, exist_ok=True)
|
|
429
|
+
except PermissionError:
|
|
430
|
+
printe(f'Permission denied to create {autosaveDir}. Use --autosave option.')
|
|
431
|
+
sys.exit(1)
|
|
427
432
|
autosaveFile = f'{autosaveDir}{prefix[:-1]}.cache'
|
|
428
433
|
C_.cachefd = open(autosaveFile, 'w')
|
|
429
434
|
printi(f'Autosave enabled. Saving to {autosaveFile}')
|
|
@@ -445,6 +450,7 @@ def init_epicsdev(prefix:str, pvDefs:list, verbose=0, serverStateChanged=None,
|
|
|
445
450
|
if putlogPV is not None:
|
|
446
451
|
_ = IFace.get(putlogPV, timeout=0.5)
|
|
447
452
|
C_.putlogPV = putlogPV
|
|
453
|
+
printi(f'caPutLog feature enabled for PV {putlogPV}')
|
|
448
454
|
except TimeoutError:
|
|
449
455
|
printw(f'WARNING: caPutLog feature will not work: PV {putlogPV} not accessible.')
|
|
450
456
|
C_.putlogPV = None
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# epicsdev
|
|
2
|
+
|
|
3
|
+
Helper module for building **EPICS PVAccess servers** using [p4p](https://github.com/epics-base/p4p).
|
|
4
|
+
|
|
5
|
+
`epicsdev` is designed for:
|
|
6
|
+
|
|
7
|
+
* Rapid PVAccess server development
|
|
8
|
+
* High-rate data simulation and stress testing
|
|
9
|
+
* GUI-based monitoring and control
|
|
10
|
+
* Rapid instrument integration
|
|
11
|
+
* AI-assisted automatic device support generation
|
|
12
|
+
|
|
13
|
+
It integrates following EPICS IOC services:<br>
|
|
14
|
+
* **Autosave**: automatically saves the values of EPICS process variables (PVs) to files on a server host, and restores those values when the server restarts.
|
|
15
|
+
* **IocStats**: provides support for PVs that show the health and status of the server, plus a few control PVs.
|
|
16
|
+
* **caPutLog**: logging of PVAccess **`put`** operations.
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -m pip install epicsdev
|
|
23
|
+
```
|
|
24
|
+
## Quick Demo
|
|
25
|
+
|
|
26
|
+
Start the demo PVAccess server:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python -m epicsdev.epicsdev
|
|
30
|
+
```
|
|
31
|
+
### Control & Visualization
|
|
32
|
+
|
|
33
|
+
Install optional GUI and plotting tools:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install pypeto pvplot
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Launch the control interface:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m pypeto -c config -f epicsdev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This provides:
|
|
46
|
+
|
|
47
|
+
* Device control panel
|
|
48
|
+
* Live waveform plots
|
|
49
|
+
* Real-time parameter monitoring
|
|
50
|
+
|
|
51
|
+
The screenshots can be seen here: [control page](docs/epicsdev_pypet.png), [plots](docs/epicsdev_pvplot.jpg).
|
|
52
|
+
|
|
53
|
+
### Phoebus Display
|
|
54
|
+
|
|
55
|
+
An example Phoebus display is provided: `config/epicsdev.bob`. [Screenshot](docs/phoebus_epicsdev.jpg).
|
|
56
|
+
|
|
57
|
+
## Multi-Channel Waveform Generator
|
|
58
|
+
|
|
59
|
+
`epicsdev.multiadc` generates high-throughput synthetic data for stress-testing EPICS systems.
|
|
60
|
+
|
|
61
|
+
For example, the following command :
|
|
62
|
+
```bash
|
|
63
|
+
python -m epicsdev.multiadc -s 0.1 -c 10000 -n 100
|
|
64
|
+
```
|
|
65
|
+
Will start a server, which generates:
|
|
66
|
+
|
|
67
|
+
* **10,000** noisy waveforms per second
|
|
68
|
+
* **100 points per waveform**
|
|
69
|
+
* **40,000 scalar parameters per second**
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
### Monitoring GUI
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python -m pypeto -c config -f multiadc
|
|
76
|
+
```
|
|
77
|
+
## Text Put Logger
|
|
78
|
+
|
|
79
|
+
`epicsdev.putlog` hosts a writable PV named `dump` and appends any written text to a file.
|
|
80
|
+
|
|
81
|
+
Start the logger server (required argument: output file path):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python -m epicsdev.putlog /tmp/putlog.txt
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Default PV prefix is `putlog0:`, so write text to:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pvput putlog0:dump "hello from client"
|
|
91
|
+
```
|
|
92
|
+
---
|
|
93
|
+
## AI-Assisted Device Support Development
|
|
94
|
+
|
|
95
|
+
`epicsdev` is structured to enable automated server generation using AI tools such as GitHub Copilot.
|
|
96
|
+
|
|
97
|
+
### Workflow Example
|
|
98
|
+
|
|
99
|
+
1. Create a new GitHub repository.
|
|
100
|
+
|
|
101
|
+
2. Provide an AI prompt such as:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
Build device support for Tektronix MSO oscilloscopes
|
|
105
|
+
using epicsdev_rigol_scope as a template and the
|
|
106
|
+
programming manual available at <PDF link>.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
3. Within ~20–40 minutes, the AI can generate a pull request.
|
|
110
|
+
|
|
111
|
+
4. Review, test, make minor corrections if needed, then merge.
|
|
112
|
+
|
|
113
|
+
### Real-World Example
|
|
114
|
+
|
|
115
|
+
Using this method, a server implementation for [Tektronix MSO oscilloscopes](https://github.com/ASukhanov/epicsdev_tektronix) was:
|
|
116
|
+
|
|
117
|
+
* ~99% correct on first generation
|
|
118
|
+
* Required only minor adjustments
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
|
|
124
|
+
* Python 3.8+
|
|
125
|
+
* p4p 4.2.2+
|
|
126
|
+
|
|
127
|
+
Optional:
|
|
128
|
+
|
|
129
|
+
* pypeto
|
|
130
|
+
* pvplot
|
|
131
|
+
* Phoebus (for .bob display files)
|
|
132
|
+
|
|
133
|
+
---
|
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
"""Helper functions for creating EPICS PVAccess server"""
|
|
2
|
+
# pylint: disable=invalid-name
|
|
3
|
+
__version__= 'v3.2.0 26-03-17'# NDArrays supported. Setters for enums were not working, recovered.
|
|
4
|
+
# SPV removed, PvDefs definitions simplified, new features added.
|
|
5
|
+
#TODO: add support for autosave, (feature 'A'), caputLog (feature 'H') and access rights
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from time import perf_counter as timer
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
import os
|
|
12
|
+
#import shelve
|
|
13
|
+
import json
|
|
14
|
+
import threading
|
|
15
|
+
from socket import gethostname
|
|
16
|
+
import psutil
|
|
17
|
+
import p4p.nt
|
|
18
|
+
from p4p.server import Server
|
|
19
|
+
from p4p.server.thread import SharedPV
|
|
20
|
+
from p4p.client.thread import Context
|
|
21
|
+
|
|
22
|
+
#``````````````````Constants
|
|
23
|
+
PeriodicUpdateInterval = 10. # seconds
|
|
24
|
+
AutosaveInterval = 10. #
|
|
25
|
+
AutosaveDefaultDirectory = '/operations/app_store/pvCache/' # Directory to save
|
|
26
|
+
# autosave files. The actual file name will be <directory><prefix>.cache
|
|
27
|
+
IFace = Context('pva')# client context for getting values from other servers
|
|
28
|
+
|
|
29
|
+
dtype2p4p = {# mapping from numpy dtype to p4p type code
|
|
30
|
+
's8':'b', 'u8':'B', 's16':'h', 'u16':'H', 'i32':'i', 'u32':'I', 'i64':'l',
|
|
31
|
+
'u64':'L', 'f32':'f', 'f64':'d', str:'s',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#``````````````````Module Storage`````````````````````````````````````````````
|
|
35
|
+
def _serverStateChanged(newState:str):
|
|
36
|
+
"""Dummy serverStateChanged function"""
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
class C_():
|
|
40
|
+
"""Storage for module members"""
|
|
41
|
+
prefix = ''
|
|
42
|
+
verbose = 0
|
|
43
|
+
startTime = 0.
|
|
44
|
+
cycle = 0
|
|
45
|
+
serverState = ''
|
|
46
|
+
PVs = {}
|
|
47
|
+
PVDefs = []
|
|
48
|
+
serverStateChanged = _serverStateChanged
|
|
49
|
+
lastCycleTime = timer()
|
|
50
|
+
lastUpdateTime = 0.
|
|
51
|
+
cycleTimeSum = 0.
|
|
52
|
+
cyclesAfterUpdate = 0
|
|
53
|
+
cachefd = None
|
|
54
|
+
lastPutTime = time.time()# last time when a put operation was performed.
|
|
55
|
+
lastAutosaveTime = 0.# last time when the cache was saved to a file.
|
|
56
|
+
putlogPV = None # name of the PV where put operations are logged. If None, then put operations are not logged.
|
|
57
|
+
|
|
58
|
+
#```````````````````Helper methods````````````````````````````````````````````
|
|
59
|
+
def serverState():
|
|
60
|
+
"""Return current server state. That is the value of the server PV, but
|
|
61
|
+
cached in C_ to avoid unnecessary get() calls."""
|
|
62
|
+
return C_.serverState
|
|
63
|
+
def _printTime():
|
|
64
|
+
return time.strftime("%m%d:%H%M%S")
|
|
65
|
+
def printi(msg):
|
|
66
|
+
"""Print info message and publish it to status PV."""
|
|
67
|
+
print(f'inf_@{_printTime()}: {msg}')
|
|
68
|
+
def printw(msg):
|
|
69
|
+
"""Print warning message and publish it to status PV."""
|
|
70
|
+
txt = f'WAR_@{_printTime()}: {msg}'
|
|
71
|
+
print(txt)
|
|
72
|
+
publish('status',txt)
|
|
73
|
+
def printe(msg):
|
|
74
|
+
"""Print error message and publish it to status PV."""
|
|
75
|
+
txt = f'ERR_{_printTime()}: {msg}'
|
|
76
|
+
print(txt)
|
|
77
|
+
publish('status',txt)
|
|
78
|
+
def _printv(msg, level):
|
|
79
|
+
if C_.verbose >= level:
|
|
80
|
+
print(f'DBG{level}: {msg}')
|
|
81
|
+
def printv(msg):
|
|
82
|
+
"""Print debug message if verbosity level >=1."""
|
|
83
|
+
_printv(msg, 1)
|
|
84
|
+
def printvv(msg):
|
|
85
|
+
"""Print debug message if verbosity level >=2."""
|
|
86
|
+
_printv(msg, 2)
|
|
87
|
+
def printv3(msg):
|
|
88
|
+
"""Print debug message if verbosity level >=3."""
|
|
89
|
+
_printv(msg, 3)
|
|
90
|
+
|
|
91
|
+
# def nt2py(nt):
|
|
92
|
+
# """Convert nt value to python value. That is to convert p4p scalar types
|
|
93
|
+
# to python scalars, and leave other types unchanged."""
|
|
94
|
+
# ntmap = {p4p.nt.scalar.ntint:int, p4p.nt.scalar.ntfloat:float,
|
|
95
|
+
# p4p.nt.scalar.ntstr:str, p4p.nt.enum.ntenum: int}
|
|
96
|
+
# return ntmap[type(nt)](nt)
|
|
97
|
+
|
|
98
|
+
def pvobj(pvName):
|
|
99
|
+
"""Return PV with given name"""
|
|
100
|
+
return C_.PVs[C_.prefix+pvName]
|
|
101
|
+
|
|
102
|
+
def pvv(pvName:str):
|
|
103
|
+
"""Return PV value"""
|
|
104
|
+
return pvobj(pvName).current()
|
|
105
|
+
|
|
106
|
+
def publish(pvName:str, value, ifChanged=False, t=None):
|
|
107
|
+
"""Publish value to PV. If ifChanged is True, then publish only if the
|
|
108
|
+
value is different from the current value. If t is not None, then use
|
|
109
|
+
it as timestamp, otherwise use current time."""
|
|
110
|
+
#print(f'Publishing {pvName} = {value}')
|
|
111
|
+
try:
|
|
112
|
+
pv = pvobj(pvName)
|
|
113
|
+
except KeyError:
|
|
114
|
+
print(f'WARNING: PV {pvName} not found. Cannot publish value.')
|
|
115
|
+
return
|
|
116
|
+
if t is None:
|
|
117
|
+
t = time.time()
|
|
118
|
+
if not ifChanged or pv.current() != value:
|
|
119
|
+
pv.post(value, timestamp=t)
|
|
120
|
+
|
|
121
|
+
def write_cache():
|
|
122
|
+
"""Write PV values to the cache file. That will be used for autosave."""
|
|
123
|
+
printv('Saving PV values to cache')
|
|
124
|
+
pvcacheMap = {}
|
|
125
|
+
for pvName, pv in C_.PVs.items():
|
|
126
|
+
if pv.writable:
|
|
127
|
+
value = pv._wrap(pv.current())['value']
|
|
128
|
+
if isinstance(value, str):
|
|
129
|
+
pyval = value
|
|
130
|
+
else:
|
|
131
|
+
# for discrete PVs, we need to save the index of the current choice, not the choice itself, because the choices can be changed in the next startup. That is a good example of using extra parameters in PV definitions.
|
|
132
|
+
try:
|
|
133
|
+
pyval = value.index
|
|
134
|
+
except Exception as e:
|
|
135
|
+
pyval = value
|
|
136
|
+
#print(f'Caching {pvName} = {value} of type {type(value)}, python value: {pyval} of type {type(pyval)}')
|
|
137
|
+
pvcacheMap[pvName[len(C_.prefix):]] = {'value': pyval, 'time': time.time()}
|
|
138
|
+
#print(f'pvCache: {pvcacheMap}')
|
|
139
|
+
C_.cachefd.seek(0)
|
|
140
|
+
json.dump(pvcacheMap, C_.cachefd)
|
|
141
|
+
C_.cachefd.truncate()
|
|
142
|
+
C_.cachefd.flush()
|
|
143
|
+
|
|
144
|
+
#``````````````````create_PVs()```````````````````````````````````````````````
|
|
145
|
+
|
|
146
|
+
def create_PVs(pvDefs, pvcache=None):
|
|
147
|
+
"""Create PVs from the definitions in pvDefs."""
|
|
148
|
+
if pvcache is None:
|
|
149
|
+
pvcache = {}
|
|
150
|
+
|
|
151
|
+
ts = time.time()
|
|
152
|
+
for defs in pvDefs:
|
|
153
|
+
try:
|
|
154
|
+
pname,desc,initial,*extra = defs
|
|
155
|
+
except ValueError:
|
|
156
|
+
printe(f'Invalid PV definition of {defs[0]}')
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
extra = extra[0] if extra else {}
|
|
159
|
+
|
|
160
|
+
#
|
|
161
|
+
iterable = type(initial) not in (int,float,str)
|
|
162
|
+
allowed_chars = 'WRAD'
|
|
163
|
+
meta = extra.get('features','')
|
|
164
|
+
writable = 'W' in meta
|
|
165
|
+
valueAlarm = extra.get('valueAlarm')
|
|
166
|
+
ntextra = [('features', p4p.nt.Type([('writable', '?')]))]
|
|
167
|
+
for ch in meta:
|
|
168
|
+
if ch not in allowed_chars:
|
|
169
|
+
printe(f'Unknown meta character {ch} in SPV definition')
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
|
|
172
|
+
if 'D' in meta:# discrete PV, that is a PV with a list of choices. The value of the PV is one of the choices. The initial value should be one of the choices or an index of the choice in the list.
|
|
173
|
+
initial = {'choices': initial, 'index': 0}
|
|
174
|
+
nt = p4p.nt.NTEnum(display=True, extra=ntextra)
|
|
175
|
+
else:
|
|
176
|
+
if isinstance(initial, np.ndarray):
|
|
177
|
+
print(f'Initial value is numpy array of shape {initial.shape} and dtype {initial.dtype}')
|
|
178
|
+
initial = initial.tolist()# convert to list
|
|
179
|
+
iterable = True
|
|
180
|
+
else:
|
|
181
|
+
# NTScalar or NTScalarArray, depending on whether initial value is iterable or not. The type is determined from the initial value, but it can be overridden by extra['type']. For discrete PVs, the type is always NTEnum, and the choices are taken from the initial value.
|
|
182
|
+
vtype = extra.get('type')
|
|
183
|
+
if vtype is None:
|
|
184
|
+
firstItem = initial[0] if iterable else initial
|
|
185
|
+
itype = type(firstItem)
|
|
186
|
+
vtype = {int:'i32', float:'f32'}.get(itype,itype)
|
|
187
|
+
tcode = dtype2p4p[vtype]
|
|
188
|
+
prefix = 'a' if iterable else ''
|
|
189
|
+
nt = p4p.nt.NTScalar(prefix+tcode, display=True, control=writable,
|
|
190
|
+
valueAlarm = valueAlarm is not None, extra=ntextra)
|
|
191
|
+
|
|
192
|
+
# If the PV value is cached in pvcache, then use the cached value as initial value. That allows to restore PV values after server restart. For discrete PVs, we need to save the index of the current choice, not the choice itself, because the choices can be changed in the next startup. That is a good example of using extra parameters in PV definitions.
|
|
193
|
+
if pname in pvcache:
|
|
194
|
+
cached = pvcache[pname]['value']
|
|
195
|
+
if isinstance(initial, dict):
|
|
196
|
+
initial['index'] = cached
|
|
197
|
+
else:
|
|
198
|
+
initial = cached
|
|
199
|
+
#printi(f'Loaded initial value for {pname} from autosave: {initial}')
|
|
200
|
+
|
|
201
|
+
#print(f'Creating PV {pname}, initial: {initial}')
|
|
202
|
+
spv = SharedPV(nt=nt, initial=initial)
|
|
203
|
+
spv.lastTimeSaved = 0.
|
|
204
|
+
spv.writable = writable
|
|
205
|
+
|
|
206
|
+
# Set initial value and description and add to the map of PVs
|
|
207
|
+
ivalue = spv.current()
|
|
208
|
+
printv((f'created pv {pname}, initial: {type(ivalue),ivalue},'
|
|
209
|
+
f'extra: {extra}'))
|
|
210
|
+
key = C_.prefix + pname
|
|
211
|
+
if key in C_.PVs:
|
|
212
|
+
printe(f'Duplicate PV name: {pname}')
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
C_.PVs[C_.prefix+pname] = spv
|
|
215
|
+
ntNamedTuples = spv._wrap(ivalue, timestamp=ts)
|
|
216
|
+
ntNamedTuples['features.writable'] = writable
|
|
217
|
+
ntNamedTuples['display.description'] = desc
|
|
218
|
+
|
|
219
|
+
# set extra parameters
|
|
220
|
+
for field in extra.keys():
|
|
221
|
+
try:
|
|
222
|
+
if field in ['limitLow','limitHigh','format','units']:
|
|
223
|
+
ntNamedTuples[f'display.{field}'] = extra[field]
|
|
224
|
+
if field.startswith('limit'):
|
|
225
|
+
ntNamedTuples[f'control.{field}'] = extra[field]
|
|
226
|
+
if field == 'valueAlarm':
|
|
227
|
+
for key,value in extra[field].items():
|
|
228
|
+
ntNamedTuples[f'valueAlarm.{key}'] = value
|
|
229
|
+
except KeyError as e:
|
|
230
|
+
print(f'ERROR. Cannot set {field} for {pname}: {e}')
|
|
231
|
+
sys.exit(1)
|
|
232
|
+
spv.post(ntNamedTuples)
|
|
233
|
+
|
|
234
|
+
if writable:
|
|
235
|
+
# add new attributes, that will be used in the put handler
|
|
236
|
+
spv.name = pname
|
|
237
|
+
spv.setter = extra.get('setter')
|
|
238
|
+
|
|
239
|
+
# add a put handler
|
|
240
|
+
@spv.put
|
|
241
|
+
def handle(spv, op):
|
|
242
|
+
vv = op.value()
|
|
243
|
+
vr = vv.raw.value
|
|
244
|
+
ntNamedTuples = spv._wrap(spv.current())
|
|
245
|
+
oldvr = ntNamedTuples['value']
|
|
246
|
+
#print(f'Put request for {spv.name} = {repr(vv)}, current value: {repr(ntNamedTuples)}')
|
|
247
|
+
# check limits, if they are defined. That will be a good
|
|
248
|
+
# example of using control structure and valueAlarm.
|
|
249
|
+
#print(f'Put request for {spv.name} = {repr(vr)}, value: {ntNamedTuples["value"]}, peer: {op.name()}, {op.peer()}, {op.account()}, {op.roles()}')
|
|
250
|
+
try:
|
|
251
|
+
limitLow = ntNamedTuples['control.limitLow']
|
|
252
|
+
limitHigh = ntNamedTuples['control.limitHigh']
|
|
253
|
+
if limitLow != limitHigh and not (limitLow <= vr <= limitHigh):
|
|
254
|
+
printw(f'Value {vr} is out of limits [{limitLow}, {limitHigh}]. Ignoring.')
|
|
255
|
+
op.done(error=f'Value out of limits [{limitLow}, {limitHigh}]')
|
|
256
|
+
return
|
|
257
|
+
except KeyError:
|
|
258
|
+
pass
|
|
259
|
+
if isinstance(vv, p4p.nt.enum.ntenum):
|
|
260
|
+
vr = str(vv)
|
|
261
|
+
if spv.setter:
|
|
262
|
+
spv.setter(vr, spv)
|
|
263
|
+
# value will be updated by the setter, so get it again
|
|
264
|
+
vr = pvv(spv.name)
|
|
265
|
+
#vr = spv._wrap(spv.current())['value']
|
|
266
|
+
printv(f'putting {spv.name} = {vr}')
|
|
267
|
+
ct = time.time()
|
|
268
|
+
C_.lastPutTime = ct
|
|
269
|
+
spv.post(vr, timestamp=ct) # update subscribers
|
|
270
|
+
|
|
271
|
+
if C_.putlogPV is not None:
|
|
272
|
+
dt = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3].split()
|
|
273
|
+
ip = op.peer().split(':')[3][:-1]# peer looks like: [::ffff:192.168.27.6]:46362
|
|
274
|
+
jmsg = {"date":dt[0], "time":dt[1],
|
|
275
|
+
"host":ip, "user":op.account(),
|
|
276
|
+
"pv":op.name(), "new":vr, "old":oldvr}
|
|
277
|
+
s = json.dumps(jmsg)
|
|
278
|
+
try:
|
|
279
|
+
IFace.put(C_.putlogPV, "'"+s+"'", timeout=0.5)# quote the string to avoid interpreting it as JSON
|
|
280
|
+
except TimeoutError:
|
|
281
|
+
printw(f'WARNING: caPutLog feature will be disabled: PV {C_.putlogPV} not accessible.')
|
|
282
|
+
C_.putlogPV = None
|
|
283
|
+
op.done()
|
|
284
|
+
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
|
285
|
+
#``````````````````Setters
|
|
286
|
+
def set_verbose(level, *_):
|
|
287
|
+
"""Set verbosity level for debugging"""
|
|
288
|
+
C_.verbose = level
|
|
289
|
+
printi(f'Setting verbose to {level}')
|
|
290
|
+
publish('verbose',level)
|
|
291
|
+
|
|
292
|
+
def set_server(servState, *_):
|
|
293
|
+
"""Example of the setter for the server PV.
|
|
294
|
+
servState can be 'Start', 'Stop', 'Exit' or 'Clear'. If servState is None,
|
|
295
|
+
then get the desired state from the server PV."""
|
|
296
|
+
#printv(f'>set_server({servState}), {type(servState)}')
|
|
297
|
+
if servState is None:
|
|
298
|
+
servState = pvv('server')
|
|
299
|
+
printi(f'Setting server state to {servState}')
|
|
300
|
+
servState = str(servState)
|
|
301
|
+
C_.serverStateChanged(servState)
|
|
302
|
+
if servState == 'Start':
|
|
303
|
+
printi('Starting the server')
|
|
304
|
+
publish('server','Started')
|
|
305
|
+
publish('status','Started')
|
|
306
|
+
elif servState == 'Stop':
|
|
307
|
+
printi('server stopped')
|
|
308
|
+
publish('server','Stopped')
|
|
309
|
+
publish('status','Stopped')
|
|
310
|
+
elif servState == 'Exit':
|
|
311
|
+
printi('server is exiting')
|
|
312
|
+
publish('server','Exited')
|
|
313
|
+
publish('status','Exited')
|
|
314
|
+
elif servState == 'Clear':
|
|
315
|
+
publish('status','Cleared')
|
|
316
|
+
# set server to previous servState
|
|
317
|
+
set_server(C_.serverState)
|
|
318
|
+
return
|
|
319
|
+
C_.serverState = servState
|
|
320
|
+
|
|
321
|
+
def create_pvDefs(pvDefs=None, pvcache=None):
|
|
322
|
+
"""Create PVs from the definitions in pvDefs and return them as a dictionary.
|
|
323
|
+
pvDefs is a list of PV definitions. Each definition is a list of 3 or 4 items:
|
|
324
|
+
[pvName, description, initialValue, extraParameters]
|
|
325
|
+
extraParameters is a dictionary with optional keys:
|
|
326
|
+
'features': string with characters W (writable), D (discrete). For example. By default, PV is read-only scalar.
|
|
327
|
+
'type': string with data type, for example 'f32', 'i32', 's8', etc. By default, the type is determined from the initial value (float -> 'f32', int -> 'i32').
|
|
328
|
+
'units': string with physical units, for example 'V', 'S', 'Mpts/s', etc.
|
|
329
|
+
'limitLow': number with low limit for the value. If defined, then the put handler will check that the value is not below the low limit.
|
|
330
|
+
'limitHigh': number with high limit for the value. If defined, then the put handler will check that the value is not above the high limit.
|
|
331
|
+
'setter': function to be called when the PV value is changed. The function should have the signature:
|
|
332
|
+
def setter(value, spv):
|
|
333
|
+
where value is the new value, and spv is the SharedPV object.
|
|
334
|
+
The PVs defined in C_.PVDefs are created first, then the PVs from pvDefs are
|
|
335
|
+
created and appended to the map of PVs. That allows to have some common PVs
|
|
336
|
+
defined in C_.PVDefs, and device-specific PVs defined in pvDefs.
|
|
337
|
+
pvcache is a dictionary with initial values for PVs. It is used for autosave.
|
|
338
|
+
The function returns a dictionary with PVs, where the keys are PV names and the values are SharedPV objects.
|
|
339
|
+
"""
|
|
340
|
+
F,T,U,LL,LH = 'features','type','units','limitLow','limitHigh'
|
|
341
|
+
C_.PVDefs = [
|
|
342
|
+
# EPICS PVs for iocStats, see https://epics.anl.gov/base/R3-14/7-docs/iocstats.html
|
|
343
|
+
['HOSTNAME', 'Server host name', gethostname()],
|
|
344
|
+
['VERSION', 'Program version', 'epicsdev '+__version__],
|
|
345
|
+
['HEARTBEAT', 'Server heartbeat, Increments once per second', 0., {U:'S'}],
|
|
346
|
+
['UPTIME', 'Server uptime in seconds', '', {U:'S'}],
|
|
347
|
+
['STARTTOD', 'Server start time', time.strftime("%m/%d/%Y %H:%M:%S")],
|
|
348
|
+
['CPU_LOAD', 'CPU load in %', 0., {U:'%'}],
|
|
349
|
+
['CA_CONN_COUNT', 'Number of TCP connections', 0],
|
|
350
|
+
# Other popular stats: CA_CLIENTS, CA_CONN_COUNT, CPU_LOAD, FD_USED, THREAD_COUNT
|
|
351
|
+
|
|
352
|
+
# Epicsdev-specific PVs
|
|
353
|
+
['status', 'Server status. Features: RWE', '', {F:'W'}],
|
|
354
|
+
['server', 'Server control. Features: RWE',
|
|
355
|
+
'Start Stop Clear Exit Started Stopped Exited'.split(),
|
|
356
|
+
{F:'WD', 'setter':set_server}],
|
|
357
|
+
['verbose', 'Debugging verbosity',
|
|
358
|
+
C_.verbose, {F:'W', T:'u8', 'setter':set_verbose, LL:0,LH:3}],
|
|
359
|
+
['sleep', 'Pause in the main loop, it could be useful for throttling the data output',
|
|
360
|
+
1.0, {F:'W', T:'f32', U:'S', LL:0.001, LH:10.1}],
|
|
361
|
+
['cycle', 'Cycle number, published every {PeriodicUpdateInterval} S.',
|
|
362
|
+
0, {T:'u32'}],
|
|
363
|
+
['cycleTime','Average cycle time including sleep, published every {PeriodicUpdateInterval} S',
|
|
364
|
+
0., {U:'S'}],
|
|
365
|
+
]
|
|
366
|
+
# append application's PVs, defined in the pvDefs and create map of
|
|
367
|
+
# providers
|
|
368
|
+
if pvDefs is not None:
|
|
369
|
+
C_.PVDefs += pvDefs
|
|
370
|
+
create_PVs(C_.PVDefs, pvcache)
|
|
371
|
+
return C_.PVs
|
|
372
|
+
|
|
373
|
+
def init_epicsdev(prefix:str, pvDefs:list, verbose=0, serverStateChanged=None,
|
|
374
|
+
listDir=None, autosaveDir=None, recall = True, putlogPV=None):
|
|
375
|
+
"""Initialize epicsdev with given prefix and PV definitions.
|
|
376
|
+
prefix is a string that will be prepended to all PV names. It should end with ':'.
|
|
377
|
+
pvDefs is a list of PV definitions, each definition is a list of 3 or 4 items:
|
|
378
|
+
[pvName, description, initialValue, extraParameters]
|
|
379
|
+
pvName is the name of the PV (without prefix)
|
|
380
|
+
description is a string with the description of the PV
|
|
381
|
+
initialValue is the initial value of the PV
|
|
382
|
+
extraParameters is a dictionary with optional keys:
|
|
383
|
+
'features': string with characters W (writable), D (discrete). For example. By default, PV is read-only scalar.
|
|
384
|
+
'type': string with data type, for example 'f32', 'i32', 's8', etc. By default, the type is determined from the initial value (float -> 'f32', int -> 'i32').
|
|
385
|
+
'units': string with physical units, for example 'V', 'S', 'Mpts/s', etc.
|
|
386
|
+
'limitLow': number with low limit for the value. If defined, then the put handler will check that the value is not below the low limit.
|
|
387
|
+
'limitHigh': number with high limit for the value. If defined, then the put handler will check that the value is not above the high limit.
|
|
388
|
+
'setter': function to be called when the PV value is changed. The function should have the signature:
|
|
389
|
+
def setter(value, spv):
|
|
390
|
+
where value is the new value, and spv is the SharedPV object.
|
|
391
|
+
verbose is an integer that controls the verbosity level for debugging.
|
|
392
|
+
serverStateChanged is a function that will be called when the server state changes. It should have the signature:
|
|
393
|
+
def serverStateChanged(newState:str):
|
|
394
|
+
where newState is the new state of the server ('Start', 'Stop', 'Exit', 'Clear').
|
|
395
|
+
listDir is a string that specifies the directory where the list of PVs will be saved. If None, then no list will be saved.
|
|
396
|
+
autosaveDir is a string that specifies the directory where the autosave file will be saved. If None, then no autosave will be performed.
|
|
397
|
+
recall is a boolean that specifies whether to load initial values from the autosave file. If False, then the initial values will be taken from the PV definitions.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
if not isinstance(verbose, int) or verbose < 0:
|
|
401
|
+
printe('init_epicsdev arguments should be (prefix:str, pvDefs:list, verbose:int, listDir:str)')
|
|
402
|
+
sys.exit(1)
|
|
403
|
+
printi(f'Initializing epicsdev with prefix {prefix}')
|
|
404
|
+
C_.prefix = prefix
|
|
405
|
+
C_.verbose = verbose
|
|
406
|
+
|
|
407
|
+
if serverStateChanged is not None:# set custom serverStateChanged function
|
|
408
|
+
C_.serverStateChanged = serverStateChanged
|
|
409
|
+
try: # check if server is already running
|
|
410
|
+
host = repr(IFace.get(prefix+'HOSTNAME', timeout=0.5)).replace("'",'')
|
|
411
|
+
print(f'ERROR: Server for {prefix} already running at {host}. Exiting.')
|
|
412
|
+
sys.exit(1)
|
|
413
|
+
except TimeoutError:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
# No existing server found. Creating PVs.
|
|
417
|
+
pvcache = {}
|
|
418
|
+
if autosaveDir == '':# autosaveDir enabled with default file name
|
|
419
|
+
autosaveDir = AutosaveDefaultDirectory
|
|
420
|
+
if recall:
|
|
421
|
+
try:
|
|
422
|
+
autosaveFile = f'{autosaveDir}{prefix[:-1]}.cache'
|
|
423
|
+
with open(autosaveFile, "r") as json_file:
|
|
424
|
+
pvcache = json.load(json_file)
|
|
425
|
+
except Exception:
|
|
426
|
+
print(f'WARNING: pvCache file {autosaveFile} not found. Using default values')
|
|
427
|
+
printv(f'AutosaveDir: {autosaveDir}, recall: {recall}')
|
|
428
|
+
if len(pvcache) == 0:
|
|
429
|
+
printi(f'Loading default values')
|
|
430
|
+
else:
|
|
431
|
+
printi(f'Loading initial values from {autosaveFile}')
|
|
432
|
+
printv(f'pvCache: {pvcache}')
|
|
433
|
+
pvs = create_pvDefs(pvDefs, pvcache)
|
|
434
|
+
# Set up autosave if requested. That will save PV values to a file, and restore them on the next startup.
|
|
435
|
+
if autosaveDir is not None:
|
|
436
|
+
try:
|
|
437
|
+
os.makedirs(autosaveDir, exist_ok=True)
|
|
438
|
+
except PermissionError:
|
|
439
|
+
printe(f'Permission denied to create {autosaveDir}. Use --autosave option.')
|
|
440
|
+
sys.exit(1)
|
|
441
|
+
autosaveFile = f'{autosaveDir}{prefix[:-1]}.cache'
|
|
442
|
+
C_.cachefd = open(autosaveFile, 'w')
|
|
443
|
+
printi(f'Autosave enabled. Saving to {autosaveFile}')
|
|
444
|
+
|
|
445
|
+
# Save list of PVs to a file, if requested
|
|
446
|
+
if listDir != '':
|
|
447
|
+
listDir = '/tmp/pvlist/' if listDir is None else listDir
|
|
448
|
+
if not os.path.exists(listDir):
|
|
449
|
+
os.makedirs(listDir)
|
|
450
|
+
filepath = f'{listDir}{prefix[:-1]}.txt'
|
|
451
|
+
printi(f'Writing list of PVs to {filepath}')
|
|
452
|
+
with open(filepath, 'w', encoding="utf-8") as f:
|
|
453
|
+
for _pvname in pvs:
|
|
454
|
+
f.write(_pvname + '\n')
|
|
455
|
+
printi(f'Hosting {len(pvs)} PVs')
|
|
456
|
+
C_.startTime = time.time()
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
if putlogPV is not None:
|
|
460
|
+
_ = IFace.get(putlogPV, timeout=0.5)
|
|
461
|
+
C_.putlogPV = putlogPV
|
|
462
|
+
except TimeoutError:
|
|
463
|
+
printw(f'WARNING: caPutLog feature will not work: PV {putlogPV} not accessible.')
|
|
464
|
+
C_.putlogPV = None
|
|
465
|
+
|
|
466
|
+
threading.Thread(target=_heartbeat_thread, daemon=True).start()
|
|
467
|
+
return pvs
|
|
468
|
+
|
|
469
|
+
def _heartbeat_thread():
|
|
470
|
+
"""Thread to update heartbeat and uptime PVs."""
|
|
471
|
+
while True:
|
|
472
|
+
time.sleep(1)
|
|
473
|
+
publish('HEARTBEAT', pvv('HEARTBEAT')+1)
|
|
474
|
+
publish('UPTIME', round(time.time() - C_.startTime, 1))
|
|
475
|
+
|
|
476
|
+
def sleep():
|
|
477
|
+
"""Sleep function to be called in the main loop. It updates cycleTime PV
|
|
478
|
+
and sleeps for the time specified in sleep PV.
|
|
479
|
+
Returns False if a periodic update occurred.
|
|
480
|
+
"""
|
|
481
|
+
time.sleep(pvv('sleep'))
|
|
482
|
+
sleeping = True
|
|
483
|
+
if serverState().startswith('Stop'):
|
|
484
|
+
return sleeping
|
|
485
|
+
tnow = timer()
|
|
486
|
+
C_.cycleTimeSum += tnow - C_.lastCycleTime
|
|
487
|
+
C_.lastCycleTime = tnow
|
|
488
|
+
C_.cyclesAfterUpdate += 1
|
|
489
|
+
C_.cycle += 1
|
|
490
|
+
printv(f'cycle {C_.cycle}')
|
|
491
|
+
if tnow - C_.lastUpdateTime > PeriodicUpdateInterval:
|
|
492
|
+
avgCycleTime = C_.cycleTimeSum / C_.cyclesAfterUpdate
|
|
493
|
+
printv(f'Average cycle time: {avgCycleTime:.6f} S.')
|
|
494
|
+
publish('cycle', C_.cycle)
|
|
495
|
+
publish('cycleTime', avgCycleTime)
|
|
496
|
+
publish('CPU_LOAD', round(psutil.cpu_percent(),1))
|
|
497
|
+
publish('CA_CONN_COUNT', len(psutil.net_connections(kind='tcp')))
|
|
498
|
+
C_.lastUpdateTime = tnow
|
|
499
|
+
C_.cycleTimeSum = 0.
|
|
500
|
+
C_.cyclesAfterUpdate = 0
|
|
501
|
+
sleeping = False
|
|
502
|
+
|
|
503
|
+
if C_.cachefd is not None and tnow - C_.lastAutosaveTime > AutosaveInterval:
|
|
504
|
+
C_.lastAutosaveTime = tnow
|
|
505
|
+
if C_.lastPutTime != 0.:
|
|
506
|
+
C_.lastPutTime = 0.
|
|
507
|
+
write_cache()
|
|
508
|
+
else:
|
|
509
|
+
printv('No changes to save')
|
|
510
|
+
return sleeping
|
|
511
|
+
|
|
512
|
+
#``````````````````Demo````````````````````````````````````````````````````````
|
|
513
|
+
if __name__ == "__main__":
|
|
514
|
+
import numpy as np
|
|
515
|
+
import argparse
|
|
516
|
+
|
|
517
|
+
def myPVDefs():
|
|
518
|
+
"""Example of PV definitions"""
|
|
519
|
+
F,T,U,LL,LH,SET = 'features','type','units','limitLow','limitHigh','setter'
|
|
520
|
+
alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
|
|
521
|
+
return [ # device-specific PVs
|
|
522
|
+
['noiseLevel', 'Noise amplitude', 1., {F:'W', U:'V'}],
|
|
523
|
+
['tAxis', 'Full scale of horizontal axis', [0.], {U:'S'}],
|
|
524
|
+
['recordLength','Max number of points',
|
|
525
|
+
100, {F:'W', T:'u32', LL:4,LH:1000000, SET:set_recordLength}],
|
|
526
|
+
['throughput', 'Performance metrics, points per second', 0., {U:'Mpts/s'}],
|
|
527
|
+
['c01Offset', 'Offset', 0., {F:'W', U:'du'}],
|
|
528
|
+
['c01VoltsPerDiv', 'Vertical scale', 0.1, {F:'W', U:'V/du'}],
|
|
529
|
+
['c01Waveform', 'Waveform array', [0.], {U:'du'}],
|
|
530
|
+
['c01Mean', 'Mean of the waveform', 0., {U:'du'}],
|
|
531
|
+
['c01Peak2Peak','Peak-to-peak amplitude', 0., {U:'du', **alarm}],
|
|
532
|
+
#['image', 'Image array', np.ndarray([0],'int16')],
|
|
533
|
+
['alarm', 'PV with alarm', 0, {U:'du', **alarm}],
|
|
534
|
+
]
|
|
535
|
+
|
|
536
|
+
pargs = None
|
|
537
|
+
rng = np.random.default_rng()
|
|
538
|
+
nPoints = 100
|
|
539
|
+
_sum = {'points': 0, 'time': 0.}
|
|
540
|
+
|
|
541
|
+
def set_recordLength(value, *_):
|
|
542
|
+
"""Record length have changed. The tAxis should be updated
|
|
543
|
+
accordingly."""
|
|
544
|
+
printi(f'Setting tAxis to {value}')
|
|
545
|
+
publish('tAxis', np.arange(value)*1.E-6)
|
|
546
|
+
publish('recordLength', value)
|
|
547
|
+
|
|
548
|
+
def init(recordLength):
|
|
549
|
+
"""Example of device initialization function"""
|
|
550
|
+
set_recordLength(recordLength)
|
|
551
|
+
|
|
552
|
+
def poll():
|
|
553
|
+
"""Example of polling function. Called every cycle when server is running.
|
|
554
|
+
It returns time, spent in publishing data"""
|
|
555
|
+
wf = rng.random(pvv('recordLength'))*pvv('noiseLevel')# it takes 5ms for 1M points
|
|
556
|
+
wf /= pvv('c01VoltsPerDiv')
|
|
557
|
+
wf += pvv('c01Offset')
|
|
558
|
+
ts = timer()
|
|
559
|
+
publish('c01Waveform', wf)
|
|
560
|
+
_sum['time'] += timer() - ts
|
|
561
|
+
_sum['points'] += len(wf)
|
|
562
|
+
publish('c01Peak2Peak', np.ptp(wf))
|
|
563
|
+
publish('c01Mean', np.mean(wf))
|
|
564
|
+
|
|
565
|
+
def periodic_update():
|
|
566
|
+
"""Perform periodic update"""
|
|
567
|
+
#printi(f'periodic update for {C_.cyclesSinceUpdate} cycles: {ElapsedTime}')
|
|
568
|
+
if state.startswith('Stop'):
|
|
569
|
+
publish('throughput', 0.)
|
|
570
|
+
else:
|
|
571
|
+
pointsPerSecond = _sum['points']/_sum['time']/1.E6
|
|
572
|
+
publish('throughput', round(pointsPerSecond,6))
|
|
573
|
+
printv(f'periodic update. Performance: {pointsPerSecond:.3g} Mpts/s')
|
|
574
|
+
_sum['points'] = 0
|
|
575
|
+
_sum['time'] = 0.
|
|
576
|
+
|
|
577
|
+
# Parse command line arguments
|
|
578
|
+
parser = argparse.ArgumentParser(description = __doc__,
|
|
579
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
580
|
+
epilog=f'{__version__}')
|
|
581
|
+
parser.add_argument('-a', '--autosave', nargs='?', default='', help=
|
|
582
|
+
'Autosave control. If not given, then autosave is enabled with default file '\
|
|
583
|
+
'name /tmp/<device><index>.cache. ' \
|
|
584
|
+
'If given without argument, then autosave is disabled' \
|
|
585
|
+
'If a file name is given, then it is used for autosave.')
|
|
586
|
+
parser.add_argument('-c', '--recall', action='store_false', help=
|
|
587
|
+
'If given: Do not load initial values from pvCache file. That is useful when you want to start with default values, but do not want to disable autosave. By default, the initial values are loaded from the cache file if it exists.')
|
|
588
|
+
parser.add_argument('-d', '--device', default='epicsDev', help=
|
|
589
|
+
'Device name, the PV name will be <device><index>:')
|
|
590
|
+
parser.add_argument('-i', '--index', default='0', help=
|
|
591
|
+
'Device index, the PV name will be <device><index>:')
|
|
592
|
+
parser.add_argument('-l', '--list', nargs='?', help=(
|
|
593
|
+
'Directory to save list of all generated PVs, if no directory is given, '
|
|
594
|
+
'then </tmp/pvlist/><prefix> is assumed.'))
|
|
595
|
+
# The rest of options are not essential, they can be controlled at runtime using PVs.
|
|
596
|
+
parser.add_argument('-n', '--npoints', type=int, default=nPoints, help=
|
|
597
|
+
'Number of points in the waveform')
|
|
598
|
+
parser.add_argument('-p', '--putlogPV', default='putlog:dump', help=
|
|
599
|
+
'Name of the PV where put operations are logged. If None, then put operations are not logged.')
|
|
600
|
+
parser.add_argument('-v', '--verbose', action='count', default=0, help=
|
|
601
|
+
'Show more log messages (-vv: show even more)')
|
|
602
|
+
pargs = parser.parse_args()
|
|
603
|
+
print(pargs)
|
|
604
|
+
|
|
605
|
+
# Initialize epicsdev and PVs
|
|
606
|
+
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
607
|
+
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose, None,
|
|
608
|
+
pargs.list, pargs.autosave, pargs.recall, pargs.putlogPV)
|
|
609
|
+
# Initialize the device using pargs if needed.
|
|
610
|
+
init(pargs.npoints)
|
|
611
|
+
|
|
612
|
+
# Start the Server. Use your set_server, if needed.
|
|
613
|
+
set_server('Start')
|
|
614
|
+
|
|
615
|
+
# Main loop
|
|
616
|
+
# In this example, we just update the waveform and its stats in a loop,
|
|
617
|
+
# but in a real application, the loop can also read data from the device,
|
|
618
|
+
# and update PVs accordingly. The loop can be paused by setting server PV to 'Stop',
|
|
619
|
+
# and exited by setting server PV to 'Exit'.
|
|
620
|
+
# The performance metrics are updated every {PeriodicUpdateInterval} seconds.
|
|
621
|
+
server = Server(providers=[PVs])
|
|
622
|
+
printi(f'Server started. Sleeping per cycle: {repr(pvv("sleep"))} S.')
|
|
623
|
+
while True:
|
|
624
|
+
state = serverState()
|
|
625
|
+
if state.startswith('Exit'):
|
|
626
|
+
break
|
|
627
|
+
if not state.startswith('Stop'):
|
|
628
|
+
poll()
|
|
629
|
+
if not sleep():# Sleep and update performance metrics periodically
|
|
630
|
+
periodic_update()
|
|
631
|
+
printi('Server is exited')
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Simulated multi-channel ADC device server using epicsdev module."""
|
|
2
|
+
# pylint: disable=invalid-name
|
|
3
|
+
__version__= 'v3.1.1 26-03-03'# updated to use new features of epicsdev v3.1.0
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from time import perf_counter as timer
|
|
7
|
+
import argparse
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .epicsdev import Server, Context, init_epicsdev, serverState, publish
|
|
11
|
+
from .epicsdev import pvv, printi, printv, set_server, sleep
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def myPVDefs():
|
|
15
|
+
"""Define PVs for the multiadc device. The PVs are defined as a list of
|
|
16
|
+
lists, where each inner list contains the PV name, description, initial
|
|
17
|
+
value, and optional dictionary of additional attributes.
|
|
18
|
+
"""
|
|
19
|
+
F,T,U,LL,LH,SET = 'features','type','units','limitLow','limitHigh','setter'
|
|
20
|
+
alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
|
|
21
|
+
pvDefs = [ # device-specific PVs
|
|
22
|
+
['channels', 'Number of device channels', pargs.channels],
|
|
23
|
+
['externalControl', 'Name of external PV, which controls the server',
|
|
24
|
+
'Start Stop Clear Exit Started Stopped Exited'.split(), {F:'WD'}],
|
|
25
|
+
['noiseLevel', 'Noise amplitude', 0.05, {F:'W', U:'V'}],
|
|
26
|
+
['tAxis', 'Full scale of horizontal axis', [0.], {U:'S'}],
|
|
27
|
+
['recordLength','Max number of points', pargs.npoints,
|
|
28
|
+
{F:'W', T:'u32', LL:4, LH:1000000, SET:set_recordLength}],
|
|
29
|
+
['alarm', 'PV with alarm', 0, {F:'WA', U:'du', **alarm}],
|
|
30
|
+
#``````````````````Auxiliary PVs
|
|
31
|
+
['timing', 'Elapsed time for waveform generation, publishing, total]', [0.], {U:'S'}],
|
|
32
|
+
['throughput', 'Total number of points processed per second', 0., {U:'Mpts/s'}],
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Templates for channel-related PVs.
|
|
36
|
+
ChannelTemplates = [
|
|
37
|
+
['c0$VoltsPerDiv', 'Vertical scale', 0.1, {F:'W', U:'V/du'}],
|
|
38
|
+
['c0$VoltOffset', 'Vertical offset', 0., {F:'W', U:'V'}],
|
|
39
|
+
['c0$Waveform', 'Waveform array', [0.], {U:'du'}],
|
|
40
|
+
['c0$Mean', 'Mean of the waveform', 0., {F:'A', U:'du'}],
|
|
41
|
+
['c0$Peak2Peak','Peak-to-peak amplitude', 0., {F:'A', U:'du', **alarm}],
|
|
42
|
+
]
|
|
43
|
+
# extend PvDefs with channel-related PVs
|
|
44
|
+
for ch in range(pargs.channels):
|
|
45
|
+
for pvdef in ChannelTemplates:
|
|
46
|
+
newpvdef = pvdef.copy()
|
|
47
|
+
newpvdef[0] = pvdef[0].replace('0$',f'{ch+1:02}')
|
|
48
|
+
if len(newpvdef) > 3:
|
|
49
|
+
newpvdef[3] = newpvdef[3].copy()
|
|
50
|
+
pvDefs.append(newpvdef)
|
|
51
|
+
return pvDefs
|
|
52
|
+
|
|
53
|
+
#``````````````````Module attributes
|
|
54
|
+
rng = np.random.default_rng()
|
|
55
|
+
ElapsedTime = {'waveform': 0., 'publish': 0., 'poll': 0.}
|
|
56
|
+
class C_():
|
|
57
|
+
cyclesSinceUpdate = 0
|
|
58
|
+
|
|
59
|
+
#``````````````````Setter functions for PVs```````````````````````````````````
|
|
60
|
+
def set_recordLength(value, *_):
|
|
61
|
+
"""Record length have changed. The tAxis should be updated accordingly."""
|
|
62
|
+
printi(f'Setting tAxis to {repr(value)}')
|
|
63
|
+
publish('tAxis', np.arange(value)*1.E-6)
|
|
64
|
+
publish('recordLength', value)
|
|
65
|
+
|
|
66
|
+
def set_externalControl(value, *_):
|
|
67
|
+
"""External control PV have changed. Control the server accordingly."""
|
|
68
|
+
pvname = str(value)
|
|
69
|
+
if pvname in (None,'0'):
|
|
70
|
+
print('External control is not activated.')
|
|
71
|
+
return
|
|
72
|
+
printi(f'External control PV: {pvname}')
|
|
73
|
+
ctxt = Context('pva')
|
|
74
|
+
try:
|
|
75
|
+
ctxt.get(pvname, timeout=0.5)
|
|
76
|
+
except TimeoutError:
|
|
77
|
+
printi(f'Cannot connect to external control PV {pvname}.')
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
|
80
|
+
def serverStateChanged(newState:str):
|
|
81
|
+
"""Start device function called when server is started"""
|
|
82
|
+
if newState == 'Start':
|
|
83
|
+
printi('start_device called')
|
|
84
|
+
elif newState == 'Stop':
|
|
85
|
+
printi('stop_device called')
|
|
86
|
+
elif newState == 'Clear':
|
|
87
|
+
printi('clear_device called')
|
|
88
|
+
publish('cycle', 0)
|
|
89
|
+
|
|
90
|
+
def init():
|
|
91
|
+
"""Device initialization function"""
|
|
92
|
+
set_recordLength(pvv('recordLength'))
|
|
93
|
+
# Set offset of each channel = channel index
|
|
94
|
+
for ch in range(pargs.channels):
|
|
95
|
+
publish(f'c{ch+1:02}VoltOffset', ch)
|
|
96
|
+
#set_externalControl(pargs.prefix + pargs.external)
|
|
97
|
+
|
|
98
|
+
def poll():
|
|
99
|
+
"""Device polling function, called every cycle when server is running"""
|
|
100
|
+
C_.cyclesSinceUpdate += 1
|
|
101
|
+
ts0 = timer()
|
|
102
|
+
for ch in range(pargs.channels):
|
|
103
|
+
ts1 = timer()
|
|
104
|
+
chstr = f'c{ch+1:02}'
|
|
105
|
+
rwf = rng.random(pvv('recordLength'))*pvv('noiseLevel')
|
|
106
|
+
wf = rwf/pvv(f'{chstr}VoltsPerDiv') + pvv(f'{chstr}VoltOffset')# the time is comparable with rng.random
|
|
107
|
+
ts2 = timer()
|
|
108
|
+
ElapsedTime['waveform'] += ts2 - ts1
|
|
109
|
+
#print(f'ElapsedTime: {C_.cyclesSinceUpdate, ElapsedTime["waveform"]}')
|
|
110
|
+
publish(f'{chstr}Waveform', wf)
|
|
111
|
+
publish(f'{chstr}Peak2Peak', np.ptp(wf))
|
|
112
|
+
publish(f'{chstr}Mean', np.mean(wf))
|
|
113
|
+
ElapsedTime['publish'] += timer() - ts2
|
|
114
|
+
ElapsedTime['poll'] += timer() - ts0
|
|
115
|
+
|
|
116
|
+
def periodic_update():
|
|
117
|
+
"""Perform periodic update"""
|
|
118
|
+
#printi(f'periodic update for {C_.cyclesSinceUpdate} cycles: {ElapsedTime}')
|
|
119
|
+
times = [(round(i/C_.cyclesSinceUpdate,6)) for i in ElapsedTime.values()]
|
|
120
|
+
publish('timing', times)
|
|
121
|
+
C_.cyclesSinceUpdate = 0
|
|
122
|
+
for key in ElapsedTime:
|
|
123
|
+
ElapsedTime[key] = 0.
|
|
124
|
+
pointsPerSecond = len(pvv('tAxis'))/(pvv('cycleTime')-pvv('sleep'))/1.E6
|
|
125
|
+
pointsPerSecond *= pvv('channels')
|
|
126
|
+
publish('throughput', round(pointsPerSecond,6))
|
|
127
|
+
printv(f'periodic update. Performance: {pointsPerSecond:.3g} Mpts/s')
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Argument parsing
|
|
131
|
+
parser = argparse.ArgumentParser(description = __doc__,
|
|
132
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
133
|
+
epilog=f'{__version__}')
|
|
134
|
+
parser.add_argument('-a', '--autosave', nargs='?', default='', help=
|
|
135
|
+
'Autosave control. If not given, then autosave is enabled with default file '\
|
|
136
|
+
'name /tmp/<device><index>.cache. ' \
|
|
137
|
+
'If given without argument, then autosave is disabled' \
|
|
138
|
+
'If a file name is given, then it is used for autosave.')
|
|
139
|
+
parser.add_argument('-c', '--recall', action='store_false', help=
|
|
140
|
+
'If given: Do not load initial values from pvCache file. That is useful when you want to start with default values, but do not want to disable autosave. By default, the initial values are loaded from the cache file if it exists.')
|
|
141
|
+
parser.add_argument('-C', '--channels', type=int, default=6, help=
|
|
142
|
+
'Number of channels per device')
|
|
143
|
+
parser.add_argument('-e', '--external', help=
|
|
144
|
+
'Name of external PV, which controls the server, if 0 then it will be <device>0:')
|
|
145
|
+
parser.add_argument('-l', '--list', default=None, nargs='?', help=
|
|
146
|
+
'Directory to save list of all generated PVs, if None, then </tmp/pvlist/><prefix> is assumed.')
|
|
147
|
+
parser.add_argument('-d', '--device', default='multiadc', help=
|
|
148
|
+
'Device name, the PV name will be <device><index>:')
|
|
149
|
+
parser.add_argument('-i', '--index', default='0', help=
|
|
150
|
+
'Device index, the PV name will be <device><index>:')
|
|
151
|
+
# The rest of arguments are not essential, they can be changed at runtime using PVs.
|
|
152
|
+
parser.add_argument('-n', '--npoints', type=int, default=100, help=
|
|
153
|
+
'Number of points in the waveform')
|
|
154
|
+
#parser.add_argument('-s', '--sleep', type=float, default=1.0, help=
|
|
155
|
+
#'Sleep time per cycle')
|
|
156
|
+
parser.add_argument('-v', '--verbose', action='count', default=0, help=
|
|
157
|
+
'Show more log messages (-vv: show even more)')
|
|
158
|
+
pargs = parser.parse_args()
|
|
159
|
+
printv(f'pargs: {pargs}')
|
|
160
|
+
|
|
161
|
+
# Initialize epicsdev and PVs
|
|
162
|
+
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
163
|
+
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose,
|
|
164
|
+
serverStateChanged, pargs.list, pargs.autosave, pargs.recall)
|
|
165
|
+
|
|
166
|
+
# Initialize the device.
|
|
167
|
+
init()
|
|
168
|
+
|
|
169
|
+
# Start the Server.
|
|
170
|
+
set_server('Start')
|
|
171
|
+
|
|
172
|
+
#``````````````````Main loop``````````````````````````````````````````````````
|
|
173
|
+
server = Server(providers=[PVs])
|
|
174
|
+
printi(f'Server started. Sleeping per cycle: {float(pvv("sleep")):.3f} S.')
|
|
175
|
+
while True:
|
|
176
|
+
state = serverState()
|
|
177
|
+
if state.startswith('Exit'):
|
|
178
|
+
break
|
|
179
|
+
if not state.startswith('Stop'):
|
|
180
|
+
poll()
|
|
181
|
+
if not sleep():
|
|
182
|
+
periodic_update()
|
|
183
|
+
printi('Server has exited')
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "epicsdev"
|
|
7
|
+
version = "3.1.5"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Andrey Sukhanov", email="sukhanov@bnl.gov" },
|
|
10
|
+
]
|
|
11
|
+
description = "Helper module for creating EPICS PVAccess servers using p4p"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"p4p>=4.2.2",
|
|
21
|
+
"psutil",
|
|
22
|
+
]
|
|
23
|
+
[project.urls]
|
|
24
|
+
"Homepage" = "https://github.com/ASukhanov/epicsdev"
|
|
25
|
+
"Bug Tracker" = "https://github.com/ASukhanov/epicsdev"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|